@tanstack/ai-preact
TanStack AI를 위한 Preact 훅으로, 헤드리스 클라이언트에 편리한 Preact 바인딩을 제공합니다.
설치
npm install @tanstack/ai-preact
useChat(options?)
완전한 타입 안전성으로 Preact에서 채팅 상태를 관리하는 기본 훅입니다.
import { useChat, fetchServerSentEvents } from "@tanstack/ai-preact";
import {
createChatClientOptions,
type InferChatMessages
} from "@tanstack/ai-client";
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
import { useState } from "preact/hooks";
const updateUIDef = toolDefinition({
name: "updateUI",
description: "Show a notification in the UI",
inputSchema: z.object({ message: z.string() }),
});
function ChatComponent() {
const [, setNotification] = useState<string | null>(null);
// Create client tool implementations
const updateUI = updateUIDef.client((input) => {
setNotification(input.message);
return { success: true };
});
// Create typed tools array (no 'as const' needed!)
const tools = [updateUI];
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents("/api/chat"),
tools,
});
// Fully typed messages!
type ChatMessages = InferChatMessages<typeof chatOptions>;
const { messages, sendMessage, isLoading, error, addToolApprovalResponse } =
useChat(chatOptions);
return <div>{/* Chat UI with typed messages */}</div>;
}
옵션
@tanstack/ai-client의 ChatClientOptions를 확장합니다.
connection- 연결 어댑터(필수)tools?- 클라이언트 도구 구현의 배열(.client()메서드 포함)initialMessages?- 초기 메시지 배열threadId?- 이 채팅의 유일한 식별자입니다. 영속성이 켜져 있으면 필수입니다. 생략하면 마운트 후 생성됩니다.forwardedProps?- AG-UIRunAgentInput.forwardedProps필드에서 서버로 전달할 클라이언트 제어 JSON입니다(예:{ provider: 'openai', model: 'gpt-5.5' }).body?- 사용 중단 예정입니다. 대신forwardedProps를 사용합니다. 이전 버전과의 호환성을 위해 계속 작동하며, 전송 시 값이forwardedProps에 병합됩니다.byok?-defineByok의 선택적 BYOK 키링입니다. 전송할 때마다 클라이언트가 확인된 provider를 준비하고x-byok-*요청 헤더를 설정합니다. 키는 절대 body에 포함되지 않습니다.byokProvider?- 이 채팅의 provider slug를 반환하는 선택적 함수입니다. slug를 반환하면 해당 키만 준비하고 전송합니다. 그렇지 않으면forwardedProps,body, 호출별sendMessagebody에서 병합된provider를 사용합니다. 나중의 소스가 우선합니다. slug가 확인되지 않으면 저장된 모든 키를 첨부하는 대신 전송 시 예외가 발생합니다.context?- 클라이언트 도구 구현에 전달되는 타입이 지정된 클라이언트 로컬 런타임 컨텍스트입니다. 이 값은 서버로 직렬화되지 않습니다.onResponse?- 응답을 수신할 때 실행되는 콜백onChunk?- 스트림 청크를 수신할 때 실행되는 콜백onFinish?- 응답이 완료될 때 실행되는 콜백onError?- 오류가 발생할 때 실행되는 콜백onInterruptStateChange?- 인터럽트 상태가 변경될 때 실행되는 콜백입니다. 복원된 상태의 컨텍스트 소스는hydrate이고, 스트리밍 또는 클라이언트에서 시작된 업데이트의 소스는live입니다.streamProcessor?- 스트림 처리 구성
참고: 클라이언트 도구는 이제 자동으로 실행되므로 onToolCall 콜백이 필요하지 않습니다.
반환값
import type { UIMessage } from "@tanstack/ai-preact";
import type { ModelMessage } from "@tanstack/ai/client";
import type {
MultimodalContent,
SendMessageOptions,
} from "@tanstack/ai-client";
interface UseChatReturn {
messages: UIMessage[];
sendMessage: (
content: string | MultimodalContent,
options?: SendMessageOptions,
) => Promise<void>;
append: (message: ModelMessage | UIMessage) => Promise<void>;
addToolResult: (result: {
toolCallId: string;
tool: string;
output: any;
state?: "output-available" | "output-error";
errorText?: string;
}) => Promise<void>;
addToolApprovalResponse: (response: {
id: string;
approved: boolean;
}) => Promise<void>;
reload: () => Promise<void>;
stop: () => void;
isLoading: boolean;
error: Error | undefined;
setMessages: (messages: UIMessage[]) => void;
clear: () => void;
}
useByok(client)
Preact에서 ByokClient 스냅샷을 구독합니다.
import { useByok } from "@tanstack/ai-preact";
import { byok } from "./byok";
export function KeyStatus() {
const snapshot = useByok(byok);
const openai = snapshot.status.openai;
const last4 = openai && "masked" in openai ? openai.masked : "No key";
return <p>{last4}</p>;
}
snapshot에는 status, locked, prompt가 있습니다. 자체 UI에서 byok.update(provider, value)를 호출해 키를 저장합니다. Bring Your Own Key를 참고합니다.
연결 어댑터
편의를 위해 @tanstack/ai-client에서 다시 내보냅니다.
import {
fetchServerSentEvents,
fetchHttpStream,
stream,
type ConnectionAdapter,
} from "@tanstack/ai-preact";
예시: 기본 채팅
import { useState } from "preact/hooks";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-preact";
export function Chat() {
const [input, setInput] = useState("");
const { messages, sendMessage, isLoading } = useChat({
connection: fetchServerSentEvents("/api/chat"),
});
const handleSubmit = (e: Event) => {
e.preventDefault();
if (input.trim() && !isLoading) {
sendMessage(input);
setInput("");
}
};
return (
<div>
<div>
{messages.map((message) => (
<div key={message.id}>
<strong>{message.role}:</strong>
{message.parts.map((part, idx) => {
if (part.type === "thinking") {
return (
<div key={idx} class="text-sm text-gray-500 italic">
💭 Thinking: {part.content}
</div>
);
}
if (part.type === "text") {
return <span key={idx}>{part.content}</span>;
}
return null;
})}
</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
value={input}
onInput={(e) => setInput(e.currentTarget.value)}
disabled={isLoading}
/>
<button type="submit" disabled={isLoading}>
Send
</button>
</form>
</div>
);
}
예시: 도구 승인
import { useChat, fetchServerSentEvents } from "@tanstack/ai-preact";
export function ChatWithApproval() {
const { messages, sendMessage, addToolApprovalResponse } = useChat({
connection: fetchServerSentEvents("/api/chat"),
});
return (
<div>
{messages.map((message) =>
message.parts.map((part) => {
if (
part.type === "tool-call" &&
part.state === "approval-requested" &&
part.approval
) {
return (
<div key={part.id}>
<p>Approve: {part.name}</p>
<button
onClick={() =>
addToolApprovalResponse({
id: part.approval!.id,
approved: true,
})
}
>
Approve
</button>
<button
onClick={() =>
addToolApprovalResponse({
id: part.approval!.id,
approved: false,
})
}
>
Deny
</button>
</div>
);
}
return null;
})
)}
</div>
);
}
예시: 타입 안전성을 갖춘 클라이언트 도구
import { useChat, fetchServerSentEvents } from "@tanstack/ai-preact";
import {
createChatClientOptions,
type InferChatMessages
} from "@tanstack/ai-client";
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
import { useState } from "preact/hooks";
const updateUIDef = toolDefinition({
name: "updateUI",
description: "Show a notification in the UI",
inputSchema: z.object({ message: z.string(), type: z.string() }),
});
const saveToStorageDef = toolDefinition({
name: "saveToStorage",
description: "Save a value to localStorage",
inputSchema: z.object({ key: z.string(), value: z.string() }),
});
export function ChatWithClientTools() {
const [notification, setNotification] = useState<{ message: string; type: string } | null>(null);
// Create client implementations
const updateUI = updateUIDef.client((input) => {
// ✅ input is fully typed!
setNotification({ message: input.message, type: input.type });
return { success: true };
});
const saveToStorage = saveToStorageDef.client((input) => {
localStorage.setItem(input.key, input.value);
return { saved: true };
});
// Create typed tools array (no 'as const' needed!)
const tools = [updateUI, saveToStorage];
const { messages, sendMessage } = useChat({
connection: fetchServerSentEvents("/api/chat"),
tools, // ✅ Automatic execution, full type safety
});
return (
<div>
{messages.map((message) =>
message.parts.map((part) => {
if (part.type === "tool-call" && part.name === "updateUI") {
// ✅ part.input and part.output are fully typed!
return <div>Tool executed: {part.name}</div>;
}
return null;
})
)}
</div>
);
}
createChatClientOptions(options)
타입이 지정된 채팅 옵션을 생성하는 헬퍼입니다(@tanstack/ai-client에서 다시 내보냅니다).
import {
createChatClientOptions,
type InferChatMessages
} from "@tanstack/ai-client";
import { fetchServerSentEvents } from "@tanstack/ai-preact";
import { tool1, tool2 } from "./tools";
// Create typed tools array (no 'as const' needed!)
const tools = [tool1, tool2];
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents("/api/chat"),
tools,
});
type Messages = InferChatMessages<typeof chatOptions>;
타입
@tanstack/ai-client에서 다시 내보냅니다.
UIMessage<TTools>- 도구 타입 매개변수가 있는 메시지 타입MessagePart<TTools>- 도구 타입 매개변수가 있는 메시지 부분TextPart- 텍스트 콘텐츠 부분ThinkingPart- 사고 콘텐츠 부분ToolCallPart<TTools>- 도구 호출 부분(판별된 유니언)ToolResultPart- 도구 결과 부분ChatClientOptions<TTools, TContext>- 타입이 지정된 클라이언트 런타임 컨텍스트를 포함한 채팅 클라이언트 옵션ConnectionAdapter- 연결 어댑터 인터페이스InferChatMessages<T>- 옵션에서 메시지 타입 추출
@tanstack/ai에서 다시 내보냅니다.
toolDefinition()- 동형 도구 정의 생성ToolDefinitionInstance- 도구 정의 타입ClientTool- 클라이언트 도구 타입ServerTool- 서버 도구 타입