branch:
client.tsx
10872 bytesRaw
import { useAgent } from "agents/react";
import { useState } from "react";
import { createRoot } from "react-dom/client";
import type { AgentState, MyAgent } from "./server";

/**
 * The OpenAI Agents SDK exports SerializedRunState (a Zod schema) from
 * @openai/agents-core, but its inferred type is deeply nested with dozens
 * of fields. This interface covers only the subset the UI actually reads.
 */
interface RunResultState {
  currentAgent?: { name: string };
  originalInput?: string;
  currentStep?: {
    type: string;
    output?: string;
    data?: { interruptions?: ToolApprovalItem[] };
  };
  lastProcessedResponse?: { toolsUsed?: string[] };
  generatedItems?: unknown[];
}

// Types for the agent state structure
interface ToolApprovalItem {
  type: "tool_approval_item";
  rawItem: {
    type: "function_call";
    id: string;
    callId: string;
    name: string;
    status: string;
    arguments: string;
    providerData: {
      id: string;
      type: "function_call";
    };
  };
  agent: {
    name: string;
  };
}

// Modal component for tool approval
function ApprovalModal({
  interruption,
  onApprove,
  onReject
}: {
  interruption: ToolApprovalItem;
  onApprove: () => void;
  onReject: () => void;
}) {
  let args: unknown;
  try {
    args = JSON.parse(interruption.rawItem.arguments);
  } catch {
    args = { raw: interruption.rawItem.arguments };
  }

  return (
    <div
      style={{
        alignItems: "center",
        backgroundColor: "rgba(0, 0, 0, 0.5)",
        bottom: 0,
        display: "flex",
        justifyContent: "center",
        left: 0,
        position: "fixed",
        right: 0,
        top: 0,
        zIndex: 1000
      }}
    >
      <div
        style={{
          backgroundColor: "white",
          borderRadius: "8px",
          boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
          maxWidth: "500px",
          padding: "24px",
          width: "90%"
        }}
      >
        <h2 style={{ color: "#333", marginTop: 0 }}>Tool Approval Required</h2>

        <div style={{ marginBottom: "16px" }}>
          <strong>Tool:</strong> {interruption.rawItem.name}
        </div>

        <div style={{ marginBottom: "16px" }}>
          <strong>Arguments:</strong>
          <pre
            style={{
              backgroundColor: "#f5f5f5",
              borderRadius: "4px",
              fontSize: "14px",
              overflow: "auto",
              padding: "8px"
            }}
          >
            {JSON.stringify(args, null, 2)}
          </pre>
        </div>

        <div style={{ marginBottom: "20px" }}>
          <strong>Agent:</strong> {interruption.agent.name}
        </div>

        <div
          style={{
            display: "flex",
            gap: "12px",
            justifyContent: "flex-end"
          }}
        >
          <button
            type="button"
            onClick={onReject}
            style={{
              backgroundColor: "white",
              border: "1px solid #dc3545",
              borderRadius: "4px",
              color: "#dc3545",
              cursor: "pointer",
              padding: "8px 16px"
            }}
          >
            Reject
          </button>
          <button
            type="button"
            onClick={onApprove}
            style={{
              backgroundColor: "#28a745",
              border: "1px solid #28a745",
              borderRadius: "4px",
              color: "white",
              cursor: "pointer",
              padding: "8px 16px"
            }}
          >
            Approve
          </button>
        </div>
      </div>
    </div>
  );
}

// Component to display agent state
function AgentStateDisplay({ state }: { state: RunResultState }) {
  const hasInterruption = state.currentStep?.type === "next_step_interruption";
  const firstInterruption = hasInterruption
    ? state.currentStep?.data?.interruptions?.[0]
    : null;

  return (
    <div style={{ marginTop: "20px" }}>
      <h3>Agent State</h3>

      <div style={{ marginBottom: "16px" }}>
        <strong>Current Agent:</strong> {state.currentAgent?.name || "Unknown"}
      </div>

      <div style={{ marginBottom: "16px" }}>
        <strong>Original Input:</strong> {state.originalInput || "None"}
      </div>

      <div style={{ marginBottom: "16px" }}>
        <strong>Current Step:</strong> {state.currentStep?.type || "Unknown"}
      </div>

      {state.currentStep?.type === "next_step_final_output" &&
        state.currentStep.output && (
          <div style={{ marginBottom: "16px" }}>
            <strong>Output:</strong>
            <div
              style={{
                backgroundColor: "#f8f9fa",
                border: "1px solid #dee2e6",
                borderRadius: "4px",
                padding: "12px",
                whiteSpace: "pre-wrap"
              }}
            >
              {state.currentStep.output}
            </div>
          </div>
        )}

      {(state.lastProcessedResponse?.toolsUsed?.length ?? 0) > 0 && (
        <div style={{ marginBottom: "16px" }}>
          <strong>Tools Used:</strong>{" "}
          {state.lastProcessedResponse?.toolsUsed?.join(", ")}
        </div>
      )}

      {(state.generatedItems?.length ?? 0) > 0 && (
        <div style={{ marginBottom: "16px" }}>
          <strong>Generated Items:</strong>
          <div
            style={{
              backgroundColor: "#f8f9fa",
              border: "1px solid #dee2e6",
              borderRadius: "4px",
              maxHeight: "200px",
              overflow: "auto",
              padding: "12px"
            }}
          >
            <pre style={{ fontSize: "12px", margin: 0 }}>
              {JSON.stringify(state.generatedItems, null, 2)}
            </pre>
          </div>
        </div>
      )}

      {hasInterruption && firstInterruption && (
        <div
          style={{
            backgroundColor: "#fff3cd",
            border: "1px solid #ffeaa7",
            borderRadius: "4px",
            marginBottom: "16px",
            padding: "12px"
          }}
        >
          <strong>⚠️ Tool Approval Required</strong>
          <div>Tool: {firstInterruption.rawItem.name}</div>
          <div>Arguments: {firstInterruption.rawItem.arguments}</div>
        </div>
      )}
    </div>
  );
}

function App() {
  const [state, setState] = useState<RunResultState | null>(null);
  const [question, setQuestion] = useState<string | null>(null);
  const [showApprovalModal, setShowApprovalModal] = useState(false);
  const [currentInterruption, setCurrentInterruption] =
    useState<ToolApprovalItem | null>(null);

  console.log("[Client] App component rendered, current state:", state);
  console.log("[Client] Current question:", question);

  const agent = useAgent<MyAgent, AgentState>({
    agent: "my-agent",
    name: "weather-chat",
    onStateUpdate({
      serialisedRunState
    }: {
      serialisedRunState: string | null;
    }) {
      console.log("[Client] onStateUpdate called with serialisedRunState:");
      if (serialisedRunState) {
        let parsedState: RunResultState;
        try {
          parsedState = JSON.parse(serialisedRunState) as RunResultState;
        } catch {
          console.error("[Client] Failed to parse serialisedRunState");
          setState(null);
          return;
        }
        console.log("[Client] Parsed state:", parsedState);
        setState(parsedState);

        // Check for interruptions
        if (parsedState?.currentStep?.type === "next_step_interruption") {
          const interruption = parsedState.currentStep.data?.interruptions?.[0];
          if (interruption) {
            console.log("[Client] Found interruption:", interruption);
            setCurrentInterruption(interruption);
            setShowApprovalModal(true);
          }
        } else if (showApprovalModal) {
          // Clear modal if state no longer has an interruption
          setShowApprovalModal(false);
          setCurrentInterruption(null);
        }
      } else {
        console.log("[Client] No serialisedRunState provided, clearing state");
        setState(null);
        setShowApprovalModal(false);
        setCurrentInterruption(null);
      }
    }
  });

  const handleAsk = () => {
    if (question) {
      console.log("[Client] Sending question to agent:", question);
      agent.stub.ask(question);
    } else {
      console.log("[Client] Attempted to ask empty question");
    }
  };

  const handleQuestionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const newQuestion = e.target.value;
    console.log("[Client] Question input changed:", newQuestion);
    setQuestion(newQuestion);
  };

  const handleApprove = () => {
    if (currentInterruption) {
      console.log(
        "[Client] Approving tool call:",
        currentInterruption.rawItem.callId
      );
      agent.stub.proceed(currentInterruption.rawItem.callId, true);
      setShowApprovalModal(false);
      setCurrentInterruption(null);
    }
  };

  const handleReject = () => {
    if (currentInterruption) {
      console.log(
        "[Client] Rejecting tool call:",
        currentInterruption.rawItem.callId
      );
      agent.stub.proceed(currentInterruption.rawItem.callId, false);
      setShowApprovalModal(false);
      setCurrentInterruption(null);
    }
  };

  // const handleCloseModal = () => {
  //   setShowApprovalModal(false);
  //   setCurrentInterruption(null);
  // };

  return (
    <div style={{ fontFamily: "Arial, sans-serif", padding: "20px" }}>
      <h1 style={{ color: "#333", marginBottom: "20px" }}>
        Weather Chat Agent
      </h1>

      <div style={{ marginBottom: "20px" }}>
        <input
          type="text"
          value={question || ""}
          onChange={handleQuestionChange}
          placeholder="Ask about the weather..."
          style={{
            border: "1px solid #ddd",
            borderRadius: "4px",
            fontSize: "16px",
            marginRight: "8px",
            padding: "8px 12px",
            width: "300px"
          }}
        />
        <button
          type="button"
          onClick={handleAsk}
          style={{
            backgroundColor: "#007bff",
            border: "none",
            borderRadius: "4px",
            color: "white",
            cursor: "pointer",
            fontSize: "16px",
            padding: "8px 16px"
          }}
        >
          Ask
        </button>
      </div>

      {state && <AgentStateDisplay state={state} />}

      {showApprovalModal && currentInterruption && (
        <ApprovalModal
          interruption={currentInterruption}
          onApprove={handleApprove}
          onReject={handleReject}
        />
      )}
    </div>
  );
}

console.log("[Client] Initializing React app");
const root = createRoot(document.getElementById("root")!);
root.render(<App />);