comark-chat
Chat
Render an AI SDK message's parts, streaming Markdown, files, and a trace of each turn's reasoning, tool calls, and sources, with the plugins your documents use.
<script setup lang="ts">
import { useChat } from "@ai-sdk/vue";
import { MessageParts } from "comark-chat/vue";
const { messages, status } = useChat();
</script>
<template>
<article v-for="(message, index) in messages" :key="message.id">
<MessageParts
:parts="message.parts"
:streaming="status === 'streaming' && index === messages.length - 1"
/>
</article>
</template>MessageParts renders each part of a message:
- Text as Markdown through
@comark/vue,@comark/react, or@comark/svelte, closing unfinished syntax while it streams. - Images and files.
- Each run of reasoning, tool calls, and sources as one trace. Its lines fold open to the thought, or to a call's input, output, and error.
It only renders. Your app sends, stores, and lays out messages, with useChat() or anything that keeps AI SDK UIMessages.
Pass the parts, and streaming for the message still arriving. While it streams, reasoning reads "Thinking" and text shows a caret.
Your plugins in messages
Section titled "Your plugins in messages"plugins, components, and options go to every Markdown part, the thoughts included, so a model can write the same blocks as your documents:
<script setup lang="ts">
import { MessageParts } from "comark-chat/vue";
import comarkUi from "comark-ui/vue";
const { plugins, components } = comarkUi();
</script>
<template>
<MessageParts :parts="message.parts" :plugins :components />
</template>Replace a kind of part
Section titled "Replace a kind of part"Each slot gets what read() made of its part, with index and the original part. The part slot replaces any part. The others replace one kind each: text, reasoning, tool, source, file, step for AI SDK's step-start, and fallback for any other part. Your data-* parts render nothing without fallback.
<MessageParts :parts="message.parts">
<template #tool="{ title, state, part }">
<ToolCard :title :state :call="part" />
</template>
<template #fallback="{ part }">
<Progress v-if="part.type === 'data-progress'" :value="part.data" />
</template>
</MessageParts>The markdown slot gets { value, streaming } and renders the Markdown of every text and reasoning part, for an app that already has a Markdown component.
React takes the same slots as functions in render. Return undefined from one to keep the default. Svelte takes them as snippets.
import { MessageParts } from "comark-chat/react";
<MessageParts parts={message.parts} render={{ tool: (step) => <ToolCard step={step} /> }} />;read(parts, streaming?)
Section titled "read(parts, streaming?)"Use read() from comark-chat to build your own renderer. It returns the blocks in order and never throws, whatever the parts hold:
| Block | What it holds |
|---|---|
text |
text, and streaming when the part or the message is still arriving. |
trace |
steps: each reasoning (title "Thinking" or "Thought", status), tool (title, status, state in words, and values, the input, output, and error as text), and source (title, href). |
file |
name, mediaType, href for a link the browser may open, download, and image for an image the page may load. |
step-start, other |
The part, for the step and fallback slots. |
A tool's status is running, waiting for the user's approval, error, or done. Its title is the part's title when it has one. Otherwise it's the tool's name with dashes and underscores as spaces, or a dynamic tool's toolName. Values show as JSON. A cyclic one shows [Circular], and one that throws shows [Value cannot be shown]. Only http and https URLs, and a named file's data URL, get a link. Images load only from http and https, data image, and blob URLs.
Package
Section titled "Package"pnpm add comark-chat @comark/vueFor React or Svelte, install @comark/react or @comark/svelte and import from comark-chat/react or comark-chat/svelte. The components add their styles on mount. On a server-rendered page, also import comark-chat/style.css.
Messages read the --comark-* tokens like the other plugins, and errors read Nuxt UI's --ui-error, then shadcn/ui's --destructive. Every class starts with cht-.