UI 컴포넌트
세션 블록 넣기
SessionList와 SessionDetail로 세션 목록과 세션 화면을 그린다
세션 블록은 API에서 데이터를 읽어 화면 한 덩어리를 그린다. 앱은 블록을 놓을 자리와 로딩·오류 모습, 링크 만드는 법만 준다. 여기서는 설치와 테마 적용에서 만든 앱에 세션 목록과 세션 화면을 넣는다.
블록이 API를 부르는 방식
블록은 @werft-ai/client의 클라이언트로 같은 origin의 /v1을 부르고, 웹 콘솔의 로그인 cookie로 인증한다. cookie 요청에 필요한 CSRF 헤더(X-Requested-With: werft-web)는 클라이언트가 붙인다. 그래서 다음이 필요하다.
- 앱과 API가 같은 origin이어야 한다. 개발 중에는 Vite의
server.proxy가/v1을 스택으로 넘긴다(설치의vite.config.ts). - 브라우저가 로그인돼 있어야 한다. cookie는 포트를 가리지 않으므로, 같은 브라우저로
http://127.0.0.1:3000웹 콘솔에 로그인하면http://127.0.0.1:5173의 앱 요청에도 그 cookie가 간다. - 보이는 세션과 할 수 있는 일은 로그인한 사람의 권한을 따른다.
앱 코드
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는 블록과 hook이 읽는 클라이언트, 언어(ko,ja,en), 링크 함수를 둔다.QueryClientProvider안에 놓는다.- 블록은 로그인한 사람과 워크스페이스별로 캐시한다.
SignedIn처럼useMe()의 응답을queryScopeOf로 바꿔QueryScopeProvider에 넘기고, 블록은 그 안에 둔다. 밖에 두면 블록이 첫 렌더에서 오류를 낸다. 로그인 전이라useMe()가 401로 실패하면 블록 대신renderError로 다시 시도를 보인다. 다른 사람이 로그인해 범위가 바뀌면 그 안의 트리를 새로 그리고 이전 사람의 캐시를 지운다. renderLink는 블록이 세션으로 가는 링크를 그릴 때 부른다. URL은 앱이 정한다. 라우터를 쓰는 앱이면 그 라우터의Link를 돌려준다.loading과renderError는 블록마다 받는다. 문구도 앱의 것이다.
실행
cd ui-demo
../node_modules/.bin/vite --host 127.0.0.1 --port 5173같은 브라우저로 웹 콘솔에 로그인한 뒤 http://127.0.0.1:5173을 연다. 목록에 로그인한 사람이 만든 세션이 보이고, 세션 id를 누르면 /sessions/<id>에서 SessionDetail이 상태, 기다리는 요청, 다음 지시 입력, 사용량, 세션 제어, 진행 이벤트를 그린다. 진행 이벤트는 이벤트 스트림으로 이어서 받는다.
로그인하지 않았으면 목록 자리에 renderError가 그린 오류와 다시 시도 버튼이 나온다.
블록
| 블록 | 그리는 것 | 받는 것(로딩·오류 말고) |
|---|---|---|
SessionList | 로그인한 사람의 세션 목록. 한 페이지씩 읽는다 | 없음 |
RecentSessions | 최근 세션 몇 개 | label, limit |
WaitingSessions | 답을 기다리는 세션 | label |
SessionDetail | 세션 한 개의 화면 | id, back, controls, unavailable, refusal |
ApprovalInbox | 답을 기다리는 세션과 그 요청. 여기서 바로 답한다 | unavailable, refusal |
unavailable은 할 수 없는 동작(answer,control,send)과 그 이유다. 이름을 준 동작은 이유와 함께 비활성으로 그린다. 서버는 어느 쪽이든 권한으로 다시 거절한다.refusal은 서버가 거절한 명령의 오류를 앱의 문구로 바꾼다.- 블록보다 작게 쓰려면
@werft-ai/react의 hook(useSessions,useSession,useSessionEvents,useSendMessage,useAnswer,useSessionControl)과@werft-ai/ui의 컴포넌트를 직접 조합한다.
다음 단계
- 승인 대기와 재개:
SessionDetail과ApprovalInbox가 답하는 요청의 종류