Skip to content

wcz-layout

ai

98 linesSource

Use when adding LLM features (chat, streamed text, structured output, tools, media generation)

Mechanics are covered by TanStack's own @tanstack/ai skills. This skill adds the wcz-layout conventions on top of that.

Rules

  • The gateway needs three server-side inputs:
    • Ensure that required env are present: AI_BASE_URL (z.url()) and AI_SUBSCRIPTION_KEY (z.string()) in src/env.ts.
    • Pass getAppToken("cognitiveServices") as the API key.
    • The subscription key sent as the Ocp-Apim-Subscription-Key header.
  • Build the adapter in one place, src/server/ai.ts, and call it per request. getAppToken caches and refreshes the token.
  • Build every chat with createLayoutChatHook(chatFn) from wcz-layout/hooks.
  • The model renders MUI components itself (show_ui) and asks the user with MUI forms (ask_user). The client sends both tools with every request, so the server function only merges them: tools: mergeAgentTools(serverTools, tools). It must also forward parentRunId and resume, or tool results and approvals never continue the run.
  • App tools are server tools (toolDefinition(...).server(...)) in serverTools; they check the signed-in user themselves. Give every tool that writes needsApproval: true: the chat shows an Approve / Deny card for it. Their calls show only the progress bar in the transcript.
  • Structured output (one typed object, e.g. a generated document) stays raw useChat with outputSchema and partial, not the chat.
  • Render streamed text outside the chat with @tanstack/markdown (install if missing): <Markdown> from @tanstack/markdown/react.

Example

ts
// src/server/ai.ts
import { createOpenaiChat } from "@tanstack/ai-openai";
import { getAppToken } from "wcz-layout/auth";
import { serverEnv } from "~/env";

export const aiText = async () =>
  createOpenaiChat("some-model", await getAppToken("cognitiveServices"), {
    baseURL: serverEnv.AI_BASE_URL,
    defaultHeaders: { "Ocp-Apim-Subscription-Key": serverEnv.AI_SUBSCRIPTION_KEY },
  });

// src/server/actions/<feature>.ts
import {
  chat,
  chatParamsFromRequestBody,
  mergeAgentTools,
  toServerSentEventsResponse,
  toolDefinition,
} from "@tanstack/ai";
import type { RunAgentInput } from "@tanstack/ai/client";
import { createServerFn } from "@tanstack/react-start";
import z from "zod";
import { authMiddleware } from "wcz-layout/middleware";
import { aiText } from "~/server/ai";

const getOrders = toolDefinition({
  name: "get_orders",
  description: "Orders of the signed-in user",
  inputSchema: z.object({ status: z.enum(["open", "closed"]) }),
}).server(async ({ status }) => findOrders(status));

export const chatFn = createServerFn({ method: "POST" })
  .validator(z.custom<RunAgentInput>())
  .middleware([authMiddleware()])
  .handler(async ({ data }) => {
    const { messages, threadId, runId, parentRunId, resume, tools } =
      await chatParamsFromRequestBody(data);
    const abortController = new AbortController();
    const stream = chat({
      adapter: await aiText(),
      threadId,
      runId,
      parentRunId,
      resume,
      systemPrompts: [SYSTEM_PROMPT],
      messages,
      tools: mergeAgentTools([getOrders], tools),
      abortController,
    });
    return toServerSentEventsResponse(stream, { abortController });
  });
tsx
// client component
import { createLayoutChatHook } from "wcz-layout/hooks";
import { chatFn } from "~/server/actions/chat";

const { useLayoutChat, LayoutChat } = createLayoutChatHook(chatFn);

const chat = useLayoutChat({ threadId: "assistant" });