sidiora-labs

Streaming Response

AI chats

About

A chat message block that reveals an AI response token-by-token while streaming, with inline numbered citations, a sources disclosure, and copy/retry/feedback actions once complete.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Streaming Response to your project and fits it to your theme.

Code

How Streaming Response is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

import StreamingResponse from "@/components/ui/streaming-response";
import { RotateCcw } from "lucide-react";
import { useReducedMotion } from "motion/react";
import { useEffect, useState } from "react";

interface DemoCitation {
  id: string;
  title: string;
  domain: string;
  url: string;
}

const PIECES = [
  "A streaming response can link directly to ",
  "Motion's React guide",
  " while the rest of the answer continues to arrive.",
  "The same response can preserve useful structure:",
  "Links stay interactive as nearby text streams",
  "Lists keep their spacing and hierarchy",
  "Code remains readable without shifting the response",
  "Set ",
  "aria-busy",
  " while new content is still arriving.",
  'const status = complete ? "ready" : "streaming";',
] as const;

const STARTS = PIECES.map((_, index) =>
  PIECES.slice(0, index).reduce((total, piece) => total + piece.length, 0),
);
const RESPONSE_LENGTH = PIECES.reduce(
  (total, piece) => total + piece.length,
  0,
);
const RESPONSE_MARKDOWN =
  "A streaming response can link directly to [Motion's React guide](https://motion.dev/docs/react) while the rest of the answer continues to arrive.\n\nThe same response can preserve useful structure:\n\n- Links stay interactive as nearby text streams\n- Lists keep their spacing and hierarchy\n- Code remains readable without shifting the response\n\nSet `aria-busy` while new content is still arriving.";

const CHARACTERS_PER_SECOND = 110;

const RESPONSE_SOURCES: DemoCitation[] = [
  {
    id: "motion-react",
    title: "Motion for React",
    domain: "motion.dev",
    url: "https://motion.dev/docs/react",
  },
  {
    id: "aria-busy",
    title: "ARIA live regions",
    domain: "developer.mozilla.org",
    url: "https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-busy",
  },
  {
    id: "react-rendering",
    title: "Rendering elements",
    domain: "react.dev",
    url: "https://react.dev/learn/conditional-rendering",
  },
];

function ResponseDemo({ onReplay }: { onReplay: () => void }) {
  const reduce = useReducedMotion() ?? false;
  const [cursor, setCursor] = useState(reduce ? RESPONSE_LENGTH : 0);
  const [complete, setComplete] = useState(reduce);

  const reveal = (index: number) =>
    PIECES[index].slice(0, Math.max(0, cursor - STARTS[index]));
  const started = (index: number) => cursor > STARTS[index];

  useEffect(() => {
    if (reduce) return;

    const startedAt = performance.now();
    let frame = 0;
    let completionTimer: number | undefined;
    const stream = (now: number) => {
      const cursor = Math.min(
        RESPONSE_LENGTH,
        Math.floor(((now - startedAt) / 1000) * CHARACTERS_PER_SECOND),
      );
      setCursor(cursor);
      if (cursor < RESPONSE_LENGTH) frame = requestAnimationFrame(stream);
      else completionTimer = window.setTimeout(() => setComplete(true), 450);
    };

    frame = requestAnimationFrame(stream);
    return () => {
      cancelAnimationFrame(frame);
      if (completionTimer) window.clearTimeout(completionTimer);
    };
  }, [reduce]);

  return (
    <StreamingResponse
      status={complete ? "complete" : "streaming"}
      copyText={RESPONSE_MARKDOWN}
      onRetry={onReplay}
      sources={RESPONSE_SOURCES}
    >
      <p>
        {reveal(0)}
        {started(1) ? (
          <a
            href="https://motion.dev/docs/react"
            target="_blank"
            rel="noreferrer noopener"
          >
            {reveal(1)}
          </a>
        ) : null}
        {reveal(2)}
      </p>
      {started(3) ? <p>{reveal(3)}</p> : null}
      {started(4) ? (
        <ul>
          <li>{reveal(4)}</li>
          {started(5) ? <li>{reveal(5)}</li> : null}
          {started(6) ? <li>{reveal(6)}</li> : null}
        </ul>
      ) : null}
      {started(7) ? (
        <p>
          {reveal(7)}
          {started(8) ? <code>{reveal(8)}</code> : null}
          {reveal(9)}
        </p>
      ) : null}
      {started(10) ? (
        <pre>
          <code>{reveal(10)}</code>
        </pre>
      ) : null}
    </StreamingResponse>
  );
}

export default function StreamingResponsePreview() {
  const [run, setRun] = useState(0);

  return (
    <div className="relative h-[500px] w-full max-w-xl bg-background text-foreground p-6">
      <ResponseDemo key={run} onReplay={() => setRun((value) => value + 1)} />
      <button
        type="button"
        onClick={() => setRun((value) => value + 1)}
        className="absolute bottom-6 left-6 inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
      >
        <RotateCcw className="size-3" />
        Replay
      </button>
    </div>
  );
}

Dependencies

More by sidiora-labs

View all
  • Preview Rail

    Preview Rail

    2
  • Expandable Tabs

    Expandable Tabs

    1
  • Morphing Modal

    Morphing Modal

    1
  • Not Found

    Not Found

  • Text Cascade

    Text Cascade

  • Smooth Scroll

    Smooth Scroll

  • Swap

    Swap

  • Motion Breadcrumb

    Motion Breadcrumb

More in AI chats

View all
  • AI Actions

    AI Actions

    324
  • Color Picker

    Color Picker

    98
  • Agent Chat

    Agent Chat

    18
  • Environment Variables

    Environment Variables

    13
  • Commit

    Commit

    19
  • AI Task List

    AI Task List

    163
  • Prompt with Model Picker

    Prompt with Model Picker

    2
  • Message Thread

    Message Thread

    42