Interop

A drop-in for the AI SDK

skeehn renders @ai-sdk/react’s useChat().messages out of the box. Pass them to <Conversation> and every UIMessage part — text, reasoning, tool calls, sources, files — maps to the right skeehn component. No glue code, and zero hard dependency on ai: the types are structural, so your bundle stays clean and the native zero-dep path still works.

Live — rendered from a real UIMessage[] transcript

What's the weather in Paris right now — and cite a source?
getWeathersuccess
{
  "city": "Paris",
  "units": "metric"
}
{
  "tempC": 18,
  "condition": "Partly cloudy",
  "humidity": 0.62
}
It's 18°C and partly cloudy in Paris right now, with humidity around 62%.

Your component

Wire useChat to your endpoint and hand the messages to <Conversation>. It autoscrolls while streaming and shows a “jump to latest” affordance when you scroll up.

tsx10 lines
1
2
3
4
5
6
7
8
9
10
'use client';
import { useChat } from '@ai-sdk/react';
import { Conversation } from '@skeehn/react/ai';
import '@skeehn/core/engine.css';

export function Chat() {
  const { messages, sendMessage } = useChat();
  // skeehn maps every UIMessage.part to the right component for you:
  return <Conversation messages={messages} />;
}

Your route handler

Standard AI SDK v5 — stream a model and return a UIMessage stream. skeehn renders whatever it emits.

ts11 lines
1
2
3
4
5
6
7
8
9
10
11
// app/api/chat/route.ts
import { streamText, convertToModelMessages } from 'ai';

export async function POST(req: Request) {
  const { messages } = await req.json();
  const result = streamText({
    model: 'anthropic/claude-sonnet-4-6',   // via the AI Gateway
    messages: convertToModelMessages(messages),
  });
  return result.toUIMessageStreamResponse();   // emits UIMessage parts
}

How parts map to components

UIMessage partskeehn component
textChatBubble (Markdown for assistant prose)
reasoningThinkingBlock
tool-* / dynamic-toolToolCard (input · output · error)
source-url / source-documentCitationCard
file (image/*)<img> · otherwise FileAttachment
step-startDivider between steps
data-*your renderData() override

Customize any part

Override a part type, or plug in your markdown renderer (react-markdown, streamdown) — everything else keeps skeehn’s defaults.

tsx7 lines
<Conversation
  messages={messages}
  renderMarkdown={(text) => <Streamdown>{text}</Streamdown>}
  components={{
    tool: (part) => <MyToolCard part={part} />,
  }}
/>

Composed primitives

Build your own surface from the same parts: <Message>, <MessageActions> (copy · regenerate · edit · feedback), <Sources>, <ModelPicker>, and <ScrollToBottomButton> — all token-themed across every skeehn theme.