UI components

Add session blocks

Render a session list and a session screen with SessionList and SessionDetail

A session block reads data from the API and renders one piece of a screen. The app supplies only where to place the block, how loading and errors look, and how to build links. Here you add a session list and a session screen to the app you made in Install and Apply the theme.

How blocks call the API

A block calls /v1 on the same origin with the client from @werft-ai/client, and authenticates with the web console's login cookie. The client adds the CSRF header that cookie requests need (X-Requested-With: werft-web). So the following are required.

  • The app and the API must share an origin. During development, Vite's server.proxy forwards /v1 to the stack (the vite.config.ts in Install).
  • The browser must be signed in. Cookies do not distinguish ports, so if you sign in to the web console at http://127.0.0.1:3000 in the same browser, that cookie is also sent with the app's requests at http://127.0.0.1:5173.
  • The sessions you see and the actions you can take follow the signed-in person's permissions.

App code

ui-demo/src/main.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createApiClient } from "@werft-ai/client";
import {
  WerftProvider,
  QueryScopeProvider,
  queryScopeOf,
  type RenderLink,
  useMe,
} from "@werft-ai/react";
import { SessionDetail, SessionList } from "@werft-ai/react/blocks";
import { ErrorState, Skeleton } from "@werft-ai/ui";
import { type ReactNode, StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./styles.css";

const client = createApiClient();
const queryClient = new QueryClient();
const renderLink: RenderLink = ({ to: _to, id, children, ...rest }) => (
  <a href={`/sessions/${id}`} {...rest}>
    {children}
  </a>
);

const loading = <Skeleton lines={3} />;
const renderError = (_error: unknown, retry: () => void) => (
  <ErrorState message="불러오지 못했다." retry={retry} retryLabel="다시 시도" />
);
const sessionId = location.pathname.match(/^\/sessions\/([^/]+)$/)?.[1];

function SignedIn({ children }: { children: ReactNode }) {
  const me = useMe();
  if (me.isError) return renderError(me.error, () => void me.refetch());
  const scope = me.data ? queryScopeOf(me.data) : null;
  if (!scope) return loading;
  return <QueryScopeProvider scope={scope}>{children}</QueryScopeProvider>;
}

createRoot(document.getElementById("root") as HTMLElement).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <WerftProvider
        client={client}
        locale="ko"
        renderLink={renderLink}
      >
        <main className="mx-auto max-w-4xl p-6">
          <SignedIn>
            {sessionId ? (
              <SessionDetail
                id={sessionId}
                back={<a href="/">세션 목록</a>}
                loading={loading}
                renderError={renderError}
              />
            ) : (
              <SessionList loading={loading} renderError={renderError} />
            )}
          </SignedIn>
        </main>
      </WerftProvider>
    </QueryClientProvider>
  </StrictMode>,
);
  • WerftProvider holds the client that blocks and hooks read, the language (ko, ja, en), and the link function. Place it inside QueryClientProvider.
  • Blocks cache per signed-in person and workspace. As SignedIn does, turn the useMe() answer into a scope with queryScopeOf, pass it to QueryScopeProvider, and put the blocks inside it. Outside it, a block throws on its first render. When useMe() fails, say with a 401 before sign-in, renderError shows a retry in place of the blocks. When someone else signs in and the scope changes, the tree inside is drawn afresh and the previous person's cache is dropped.
  • renderLink is called when a block renders a link to a session. The app decides the URL. In an app with a router, return that router's Link.
  • loading and renderError are passed to each block. The wording is also the app's own.

Run

cd ui-demo
../node_modules/.bin/vite --host 127.0.0.1 --port 5173

Sign in to the web console in the same browser, then open http://127.0.0.1:5173. The list shows the sessions the signed-in person created, and when you click a session id, SessionDetail at /sessions/<id> renders the status, waiting requests, an input for follow-up instructions, usage, session controls, and progress events. Progress events keep arriving over the event stream.

If you are not signed in, the error rendered by renderError and a retry button appear where the list would be.

Blocks

BlockWhat it rendersWhat it takes (besides loading and error)
SessionListThe signed-in person's session list. It reads one page at a timeNone
RecentSessionsA few recent sessionslabel, limit
WaitingSessionsSessions waiting for an answerlabel
SessionDetailThe screen of one sessionid, back, controls, unavailable, refusal
ApprovalInboxSessions waiting for an answer and their requests. You answer right hereunavailable, refusal
  • unavailable lists the actions that cannot be done (answer, control, send) and the reason. An action you name is rendered disabled with its reason. Whether or not the UI disables it, the server checks permissions again and rejects a request the person may not make.
  • refusal turns the error of a command the server rejected into the app's own wording.
  • To use something smaller than a block, combine the hooks of @werft-ai/react (useSessions, useSession, useSessionEvents, useSendMessage, useAnswer, useSessionControl) and the components of @werft-ai/ui directly.

Next steps

On this page