Skip to content

Repository files navigation

md-humanizer

md-humanizer is a presentation layer for AI responses. It keeps Markdown and structured response metadata separate from the visual treatment, so the same AI output can appear as a readable document, a compact assistant message, or a handwritten sketch.

Install

npm install md-humanizer

The smallest integration

import { HumanizedMessage } from "md-humanizer";
import "md-humanizer/styles.css";

export function AssistantMessage({ content }: { content: string }) {
  return <HumanizedMessage content={content} />;
}

HumanizedMessage is an alias for HumanizedMarkdown. Existing Markdown works immediately, including GFM tables, task lists, links, fenced code, and partial streaming content.

Vercel AI SDK and AI SDK UI

The optional md-humanizer/ai-sdk entry point accepts the UIMessage objects returned by useChat from @ai-sdk/react. It reads the message parts array, humanizes text parts while they stream, and can map source-url and source-document parts into the renderer's source list.

Install the AI SDK packages in the application that uses this adapter:

npm install ai @ai-sdk/react

Then render assistant messages directly:

import { useChat } from "@ai-sdk/react";
import { HumanizedAIMessage } from "md-humanizer/ai-sdk";
import "md-humanizer/styles.css";

export function ChatMessages() {
  const { messages, status } = useChat();
  const lastMessageId = messages.at(-1)?.id;

  return messages.map((message) =>
    message.role === "assistant" ? (
      <HumanizedAIMessage
        key={message.id}
        message={message}
        streaming={message.id === lastMessageId && status === "streaming"}
        preset="sketch"
        showSources
      />
    ) : (
      <div key={message.id}>
        {message.parts
          .filter((part) => part.type === "text")
          .map((part) => part.text)
          .join("")}
      </div>
    ),
  );
}

The adapter intentionally does not invent UI for tool calls, reasoning, files, or custom data parts. Keep those in the host application, or render them with the optional escape hatch:

<HumanizedAIMessage
  message={message}
  renderPart={(part) => {
    if (part.type === "data-weather") {
      return <WeatherCard data={part.data} />;
    }
    return null;
  }}
/>

On the server, use the normal AI SDK UI message stream response. The renderer does not require a special endpoint:

const result = streamText({
  model,
  messages: await convertToModelMessages(messages),
});

return result.toUIMessageStreamResponse();

The adapter is available from md-humanizer/ai-sdk so the base renderer does not force AI SDK onto applications that do not use it. It also exports getAIMessageText and toHumanizedDocument for custom message layouts.

Prompting the runtime model

The renderer can only humanize syntax that the model emits. Import the maintained prompt contract instead of copying formatting instructions into every application:

import { HUMANIZER_SYSTEM_PROMPT } from "md-humanizer/prompt";

const result = streamText({
  model,
  system: `${APPLICATION_SYSTEM_PROMPT}\n\n${HUMANIZER_SYSTEM_PROMPT}`,
  messages: await convertToModelMessages(messages),
});

composeHumanizerSystemPrompt(APPLICATION_SYSTEM_PROMPT) is available when you prefer a helper. The full portable contract is documented in docs/humanizer-format.md, with copyable instructions for coding agents in agent-instructions/.

Presets

<HumanizedMarkdown content={answer} preset="reading" />
<HumanizedMarkdown content={answer} preset="sketch" />
<HumanizedMarkdown content={answer} preset="compact" />
  • reading is the accessible default for longer answers.
  • sketch uses the handwritten visual language from the product direction.
  • compact is designed for sidebars, coding assistants, and narrow panels.

The old theme="paper" | "ink" | "quiet" API remains supported as a compatibility alias.

Structured AI documents

The renderer accepts a plain string or a provider-neutral document object:

<HumanizedMessage
  preset="sketch"
  content={{
    markdown: "The migration is **ready**.",
    status: "complete",
    blocks: [
      { type: "summary", title: "In short", content: "The core path is ready." },
      { type: "steps", title: "Next steps", items: ["Ship it", "Watch errors"] },
    ],
    sources: [
      {
        id: "docs",
        title: "Documentation",
        url: "https://example.com/docs",
        domain: "example.com",
      },
    ],
  }}
/>

Supported block types are summary, callout, tip, warning, decision, and steps. Use renderBlock when an application needs to replace the default presentation.

Readability settings

The package intentionally renders no product controls or response chrome. There is no built-in Copy button, raw-Markdown toggle, retry action, share action, or response background; the host AI app owns those controls.

<HumanizedMarkdown
  content={answer}
  settings={{
    fontScale: 1.1,
    lineHeight: "spacious",
    contrast: "high",
    motion: "reduced",
  }}
/>

Named font-size variants scale the complete presentation rhythm, including text, headings, spacing, and chart labels. small is 0.85x, medium is 1x, and large is intentionally 3x:

<HumanizedMarkdown content={answer} settings={{ fontSize: "large" }} />

For an in-between value, use fontScale from 0.5 to 4. If both are provided, the named fontSize wins.

Sources are opt-in content as well:

<HumanizedMarkdown content={answer} showSources />

Set streaming on string content, or use status: "streaming" on a document. Streaming disables decorative chart motion and exposes aria-busy on the response region.

Intentional emphasis

The model can emit portable inline syntax:

Five years side by side — ==NVIDIA== is up ^^+874%^^.

Or the host app can pass a sidecar annotation result without changing the model's Markdown:

<HumanizedMarkdown
  content={answer}
  annotations={[
    { type: "highlight", text: "NVIDIA" },
    { type: "circle", text: "+874%" },
  ]}
/>

Bold Markdown receives a subtle emphasis treatment automatically. Precise highlights and circles should come from structured model output, a tool result, or application policy rather than a renderer guessing at factual importance.

SVG charts with an accessible fallback

```human-chart
{
  "type": "line",
  "title": "Five-year performance",
  "description": "NVIDIA rises from 0 to 874 percent while AMD rises from 0 to 453 percent.",
  "x": ["2022", "2023", "2024", "2025", "2026"],
  "series": [
    { "label": "NVIDIA", "color": "#239a60", "values": [0, 80, 280, 540, 874] },
    { "label": "AMD", "color": "#3776d7", "values": [0, 55, 100, 180, 453] }
  ]
}
```

Charts render as inline SVG, expose a text summary, and can include a keyboard-accessible data table:

<HumanizedMarkdown content={answer} showChartData />

Invalid chart payloads remain visible as code instead of silently disappearing.

Google Fonts

For a convenient runtime loader:

<HumanizedMarkdown
  content={answer}
  preset="sketch"
  googleFont={{
    family: "Just Another Hand",
    weights: [400],
    subsets: ["latin"],
    display: "swap",
  }}
/>

The shorthand is also supported:

<HumanizedMarkdown
  content={answer}
  preset="sketch"
  googleFont="Just Another Hand"
/>

The shorthand both loads the Google Fonts CSS2 stylesheet and assigns Just Another Hand to the renderer. This font currently exposes weight 400, so no weight option is necessary for the shorthand. The runtime request is intentionally explicit because it has network, privacy, CSP, and layout-shift implications.

For Next.js, use next/font/google for build-time self-hosting and pass the generated family into font:

import { Just_Another_Hand } from "next/font/google";

const justAnotherHand = Just_Another_Hand({ subsets: ["latin"], weight: "400" });

<HumanizedMarkdown
  content={answer}
  className={justAnotherHand.className}
  font={justAnotherHand.style.fontFamily}
/>;

The package also exports GoogleFont, buildGoogleFontUrl, and cssFontFamily for app-level control.

Customization

.my-ai-chat .mdh {
  --mdh-highlight: rgba(255, 210, 70, 0.55);
  --mdh-ink: #202020;
  --mdh-link: #245da8;
}

Use components to customize Markdown elements and renderBlock to customize structured blocks. The renderer sanitizes embedded HTML and keeps code, links, and data semantically represented.

Development

npm install
npm run check

About

A presentation layer for AI responses. It keeps Markdown and structured response metadata separate from the visual treatment, so the same AI output can appear as a readable document, a compact assistant message, or a handwritten sketch.

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Contributors

Languages