branch:
app.tsx
20243 bytesRaw
import "./app.css";

import { useAgent } from "agents/react";
import { nanoid } from "nanoid";
import { useCallback, useEffect, useMemo, useState } from "react";
import sequentialCode from "./flows/01 sequential.txt?raw";
import routingCode from "./flows/02 routing.txt?raw";
import parallelCode from "./flows/03 parallel.txt?raw";
import orchestratorCode from "./flows/04 orchestrator.txt?raw";
import evaluatorCode from "./flows/05 evaluator.txt?raw";

type ToastType = "success" | "error" | "info";

type Toast = {
  id: number;
  type: ToastType;
  message: string;
};

type WorkflowStatus = {
  isRunning: boolean;
  output: string;
};

type WorkflowType =
  | "sequential"
  | "routing"
  | "parallel"
  | "orchestrator"
  | "evaluator";

type PatternProps = {
  type: WorkflowType;
  title: string;
  description: string;
  image: string;
  code: string;
  index: number;
};

type FormState = {
  sequential: { input: string };
  routing: { query: string };
  parallel: { code: string };
  orchestrator: { featureRequest: string };
  evaluator: { text: string; targetLanguage: string };
};

const LANGUAGES = [
  { label: "French", value: "french" },
  { label: "Spanish", value: "spanish" },
  { label: "Japanese", value: "japanese" },
  { label: "German", value: "german" },
  { label: "Mandarin Chinese", value: "mandarin" },
  { label: "Arabic", value: "arabic" },
  { label: "Russian", value: "russian" },
  { label: "Italian", value: "italian" },
  { label: "Klingon", value: "klingon" },
  { label: "Portuguese", value: "portuguese" }
] as const;

function Toast({ toast, onClose }: { toast: Toast; onClose: () => void }) {
  useEffect(() => {
    const timer = setTimeout(() => {
      onClose();
    }, 5000);

    return () => clearTimeout(timer);
  }, [onClose]);

  const getIcon = (type: ToastType) => {
    switch (type) {
      case "success":
        return "✅";
      case "error":
        return "❌";
      case "info":
        return "ℹ️";
    }
  };

  return (
    <div className={`toast ${toast.type}`}>
      <span className="toast-icon">{getIcon(toast.type)}</span>
      <span className="toast-message">{toast.message}</span>
      <button type="button" className="toast-close" onClick={onClose}>
        ✕
      </button>
    </div>
  );
}

function ToastContainer({
  toasts,
  onClose
}: {
  toasts: Toast[];
  onClose: (id: number) => void;
}) {
  return (
    <div className="toast-container">
      {toasts.map((toast) => (
        <Toast key={toast.id} toast={toast} onClose={() => onClose(toast.id)} />
      ))}
    </div>
  );
}

function getOrCreateSessionId() {
  const stored = globalThis.localStorage?.getItem("sessionId");
  if (stored) return stored;

  const newId = nanoid(8);
  globalThis.localStorage?.setItem("sessionId", newId);
  return newId;
}

function PatternSection({
  type,
  title,
  description,
  image,
  code,
  index,
  sessionId
}: PatternProps & { sessionId: string }) {
  const [activeTab, setActiveTab] = useState<"diagram" | "code">("diagram");
  const [isCodeExpanded, setIsCodeExpanded] = useState(false);

  const socket = useAgent({
    agent: type,
    name: sessionId,
    onMessage: (e) => {
      const data = JSON.parse(e.data);
      switch (data.type) {
        case "status":
          setWorkflowStatus(data.status);
          break;
        case "toast": {
          const event = new CustomEvent("showToast", {
            detail: {
              message: data.toast.message,
              type: data.toast.type as ToastType
            }
          });
          window.dispatchEvent(event);
          break;
        }
      }
    },
    prefix: "agents"
  });

  const [workflowStatus, setWorkflowStatus] = useState<WorkflowStatus>({
    isRunning: false,
    output: ""
  });

  const [formState, setFormState] = useState<FormState[typeof type]>(() => {
    switch (type) {
      case "sequential":
        return { input: "Our new AI-powered productivity app" };
      case "routing":
        return { query: "How do I reset my password?" };
      case "parallel":
        return {
          code: `function processUserData(data) {
  // TODO: Add validation
  database.save(data);
  return true;
}`
        };
      case "orchestrator":
        return {
          featureRequest:
            "Add dark mode support to the dashboard, including theme persistence and system preference detection"
        };
      case "evaluator":
        return {
          targetLanguage: LANGUAGES[0].value,
          text: "The early bird catches the worm"
        };
    }
  });

  const handleInputChange = (
    e: React.ChangeEvent<
      HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
    >
  ) => {
    const { name, value } = e.target;
    setFormState((prev) => ({ ...prev, [name]: value }));
  };

  const getFormContent = () => {
    if (type === "sequential") {
      const state = formState as FormState["sequential"];
      return (
        <div className="form-group">
          <label htmlFor="sequential-input">Marketing Copy Input</label>
          <input
            id="sequential-input"
            type="text"
            name="input"
            value={state.input}
            onChange={handleInputChange}
            placeholder="e.g., 'Our new AI-powered productivity app'"
            className="workflow-input"
          />
          <small className="input-help">
            Enter a product or service to generate marketing copy for
          </small>
        </div>
      );
    }

    if (type === "routing") {
      const state = formState as FormState["routing"];
      return (
        <div className="form-group">
          <label htmlFor="routing-query">Customer Query</label>
          <input
            id="routing-query"
            type="text"
            name="query"
            value={state.query}
            onChange={handleInputChange}
            placeholder="e.g., 'How do I reset my password?'"
            className="workflow-input"
          />
          <small className="input-help">
            Enter a customer support question to be routed
          </small>
        </div>
      );
    }

    if (type === "parallel") {
      const state = formState as FormState["parallel"];
      return (
        <div className="form-group">
          <label htmlFor="parallel-code">Code for Review</label>
          <textarea
            id="parallel-code"
            name="code"
            value={state.code}
            onChange={handleInputChange}
            placeholder={
              "e.g.,\nfunction processUserData(data) {\n  // TODO: Add validation\n  database.save(data);\n  return true;\n}"
            }
            className="workflow-input workflow-textarea"
            rows={4}
          />
          <small className="input-help">
            Enter code snippet for parallel security, performance, and
            maintainability review
          </small>
        </div>
      );
    }

    if (type === "orchestrator") {
      const state = formState as FormState["orchestrator"];
      return (
        <div className="form-group">
          <label htmlFor="orchestrator-request">Feature Request</label>
          <textarea
            id="orchestrator-request"
            name="featureRequest"
            value={state.featureRequest}
            onChange={handleInputChange}
            placeholder="e.g., 'Add dark mode support to the dashboard, including theme persistence and system preference detection'"
            className="workflow-input workflow-textarea"
            rows={4}
          />
          <small className="input-help">
            Describe the feature to be implemented across multiple files
          </small>
        </div>
      );
    }

    if (type === "evaluator") {
      const state = formState as FormState["evaluator"];
      return (
        <>
          <div className="form-group">
            <label htmlFor="evaluator-text">Text to Translate</label>
            <textarea
              id="evaluator-text"
              name="text"
              value={state.text}
              onChange={handleInputChange}
              placeholder="e.g., 'The early bird catches the worm'"
              className="workflow-input workflow-textarea"
              rows={4}
            />
            <small className="input-help">
              Enter text to be translated and optimized
            </small>
          </div>
          <div className="form-group">
            <label htmlFor="evaluator-language">Target Language</label>
            <select
              id="evaluator-language"
              name="targetLanguage"
              value={state.targetLanguage}
              onChange={handleInputChange}
              className="workflow-input"
            >
              {LANGUAGES.map((lang) => (
                <option key={lang.value} value={lang.value}>
                  {lang.label}
                </option>
              ))}
            </select>
            <small className="input-help">
              Select the language to translate into
            </small>
          </div>
        </>
      );
    }
  };

  const formatOutput = (output: string) => {
    try {
      // Try to parse as JSON first
      const parsed = JSON.parse(output);
      return JSON.stringify(parsed, null, 2);
    } catch {
      // If not JSON, return as is
      return output;
    }
  };

  const runWorkflow = async () => {
    setWorkflowStatus((prev) => ({ ...prev, isRunning: true }));

    try {
      socket.send(
        JSON.stringify({
          input: formState,
          type: "run"
        })
      );
      // Show success toast when workflow starts
      const event = new CustomEvent("showToast", {
        detail: { message: `Started ${title} workflow...`, type: "info" }
      });
      window.dispatchEvent(event);
    } catch (_error) {
      // Show error toast if something goes wrong
      const event = new CustomEvent("showToast", {
        detail: { message: `Failed to start ${title} workflow`, type: "error" }
      });
      window.dispatchEvent(event);
    }
  };

  return (
    <section className="pattern-section">
      <h2>
        {index + 1}. {title}
      </h2>
      <div className="pattern-content">
        <div className="tab-container">
          <div className="tab-buttons">
            <button
              type="button"
              className={`tab-button ${
                activeTab === "diagram" ? "active" : ""
              }`}
              onClick={() => setActiveTab("diagram")}
            >
              Diagram
            </button>
            <button
              type="button"
              className={`tab-button ${activeTab === "code" ? "active" : ""}`}
              onClick={() => setActiveTab("code")}
            >
              Code
            </button>
          </div>
          <div className="tab-content">
            <div
              className={`tab-pane ${activeTab === "diagram" ? "active" : ""}`}
            >
              <div className="pattern-image">
                <img src={image} alt={`${title} workflow diagram`} />
              </div>
            </div>
            <div className={`tab-pane ${activeTab === "code" ? "active" : ""}`}>
              <div className="code-tab-container">
                <div
                  className={`code-content ${isCodeExpanded ? "expanded" : ""}`}
                >
                  {code}
                </div>
                <button
                  type="button"
                  className="expand-button"
                  onClick={() => setIsCodeExpanded(!isCodeExpanded)}
                >
                  {isCodeExpanded ? "Collapse" : "Expand"}
                  <span className={`icon ${isCodeExpanded ? "expanded" : ""}`}>
                    ▼
                  </span>
                </button>
              </div>
            </div>
          </div>
        </div>
        <p className="pattern-description">{description}</p>
        <div className="workflow-runner">
          <div className="workflow-form">{getFormContent()}</div>
          <div className="workflow-toolbar">
            <button
              type="button"
              className="run-button"
              onClick={runWorkflow}
              disabled={workflowStatus.isRunning}
            >
              {workflowStatus.isRunning ? (
                <>
                  <div className="spinner" />
                  Running...
                </>
              ) : workflowStatus.output ? (
                "Run Again"
              ) : (
                "Run"
              )}
            </button>
            {/* {workflowState.isRunning && (
              <button
                className="stop-button"
                onClick={() => {
                  socket.send(JSON.stringify({ type: "stop" }));
                  const event = new CustomEvent("showToast", {
                    detail: {
                      type: "info",
                      message: `Stopping ${title} workflow...`,
                    },
                  });
                  window.dispatchEvent(event);
                }}
              >
                Stop
              </button>
            )} */}
          </div>
          <pre className="workflow-output">
            {workflowStatus.output
              ? formatOutput(workflowStatus.output)
              : `Enter input above and click 'Run' to see ${title} in action`}
          </pre>
        </div>
      </div>
    </section>
  );
}

export default function App() {
  const [theme, setTheme] = useState<"light" | "dark">("light");
  const [toasts, setToasts] = useState<Toast[]>([]);
  const sessionId = useMemo(() => getOrCreateSessionId(), []);

  const addToast = useCallback((type: ToastType, message: string) => {
    const id = Date.now();
    setToasts((prev) => [...prev, { id, message, type }]);
  }, []);

  const removeToast = (id: number) => {
    setToasts((prev) => prev.filter((toast) => toast.id !== id));
  };

  useEffect(() => {
    // Theme detection code
    const prefersDark = window.matchMedia(
      "(prefers-color-scheme: dark)"
    ).matches;
    setTheme(prefersDark ? "dark" : "light");
    document.documentElement.setAttribute(
      "data-theme",
      prefersDark ? "dark" : "light"
    );

    // Add toast event listener
    const handleToast = (
      e: CustomEvent<{ type: ToastType; message: string }>
    ) => {
      addToast(e.detail.type, e.detail.message);
    };

    window.addEventListener("showToast", handleToast as EventListener);

    return () => {
      window.removeEventListener("showToast", handleToast as EventListener);
    };
  }, [addToast]);

  const toggleTheme = () => {
    const newTheme = theme === "light" ? "dark" : "light";
    setTheme(newTheme);
    document.documentElement.setAttribute("data-theme", newTheme);
  };

  const patterns = {
    evaluator: {
      code: evaluatorCode,
      description:
        "One LLM generates responses while another provides evaluation and feedback in a loop.",
      image: "/flows/05 evaluator.png",
      title: "Evaluator-Optimizer"
    },
    orchestrator: {
      code: orchestratorCode,
      description:
        "A central LLM dynamically breaks down tasks, delegates to worker LLMs, and synthesizes results.",
      image: "/flows/04 orchestrator.png",
      title: "Orchestrator-Workers"
    },
    parallel: {
      code: parallelCode,
      description:
        "Enables simultaneous task processing through sectioning or voting mechanisms.",
      image: "/flows/03 parallel.png",
      title: "Parallelization"
    },
    routing: {
      code: routingCode,
      description:
        "Classifies input and directs it to specialized followup tasks, allowing for separation of concerns.",
      image: "/flows/02 routing.png",
      title: "Routing"
    },
    sequential: {
      code: sequentialCode,
      description:
        "Decomposes tasks into a sequence of steps, where each LLM call processes the output of the previous one.",
      image: "/flows/01 sequential.png",
      title: "Prompt Chaining"
    }
  };

  return (
    <div className="container">
      <ToastContainer toasts={toasts} onClose={removeToast} />
      <header>
        {/* oxlint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- theme toggle */}
        <div className="theme-toggle" onClick={toggleTheme}>
          <span className="theme-toggle-icon">
            {theme === "light" ? "🌞" : "🌙"}
          </span>
          <div className="theme-toggle-switch" />
        </div>
        <h1>⛅️ Building Effective Agents</h1>
        <p>Common patterns for implementing AI agents</p>
        <div className="header-links">
          <p>
            Based on{" "}
            <a
              href="https://www.anthropic.com/research/building-effective-agents"
              target="_blank"
              rel="noopener noreferrer"
            >
              Anthropic's research
            </a>{" "}
            on agent patterns.
          </p>
          <p>
            Code samples from{" "}
            <a
              href="https://sdk.vercel.ai/docs/foundations/agents"
              target="_blank"
              rel="noopener noreferrer"
            >
              AI SDK
            </a>
            , running on{" "}
            <a
              href="https://github.com/cloudflare/agents"
              target="_blank"
              rel="noopener noreferrer"
            >
              Cloudflare Agents
            </a>
            .
          </p>
        </div>
      </header>

      <main>
        {(
          Object.entries(patterns) as [
            WorkflowType,
            (typeof patterns)[keyof typeof patterns]
          ][]
        ).map(([type, pattern], index) => (
          <PatternSection
            key={type}
            type={type}
            title={pattern.title}
            description={pattern.description}
            image={pattern.image}
            code={pattern.code}
            index={index}
            sessionId={sessionId}
          />
        ))}
      </main>

      <section className="durable-objects-intro">
        <h2>Why Durable Objects?</h2>
        <p>
          Cloudflare's Durable Objects provide the perfect environment for
          hosting AI agents:
        </p>

        <div className="benefits-grid">
          <div className="benefit-card">
            <h3>Persistent State</h3>
            <p>
              Agents continue running even when browser tabs are closed or
              refreshed, maintaining their state and context throughout
              long-running tasks.
            </p>
          </div>

          <div className="benefit-card">
            <h3>Real-time Updates</h3>
            <p>
              WebSocket connections enable live streaming of agent progress,
              thoughts, and results directly to any connected client, providing
              immediate feedback.
            </p>
          </div>

          <div className="benefit-card">
            <h3>Global Scale</h3>
            <p>
              Agents run at the edge, automatically scaling across Cloudflare's
              worldwide network, ensuring low-latency responses regardless of
              user location.
            </p>
          </div>

          <div className="benefit-card">
            <h3>Flexible Triggers</h3>
            <p>
              Agents can be activated through various means: HTTP requests,
              scheduled cron jobs, email handlers, or other server-side events.
            </p>
          </div>

          <div className="benefit-card">
            <h3>Memory Isolation</h3>
            <p>
              Each agent runs in its own isolated environment, preventing
              resource contention and ensuring reliable performance.
            </p>
          </div>

          <div className="benefit-card">
            <h3>Cost Effective</h3>
            <p>
              Pay only for the compute time your agents use, with no idle costs
              and automatic scaling based on demand.
            </p>
          </div>
        </div>
      </section>
    </div>
  );
}