Back to roadmaps vercel-ai-sdk Course

Project: Markdown-Enabled Stream Chatbot

In this project, we will build a production-ready streaming chatbot. The interface will render raw markdown outputs (including tables and lists) and syntax-highlight code snippets with a copy-to-clipboard action button.


1. Installation of Rendering libraries

To render markdown and highlight code syntax, install these packages:

# Install react-markdown and code highlighter plugins
npm install react-markdown remark-gfm

2. Implementing the React Chat Component

Create the chat interface module:

// app/chatbot/MarkdownChat.tsx
"use client";

import React from "react";
import { useChat } from "ai/react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";

export default function MarkdownChat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat({
    api: "/api/chat",
  });

  // Helper function to handle code clipboard copying
  function copyToClipboard(text: string) {
    navigator.clipboard.writeText(text);
    alert("Code copied to clipboard!");
  }

  return (
    <div className="flex flex-col h-[650px] max-w-2xl mx-auto border rounded-2xl bg-white shadow-lg p-6">
      <h2 className="text-xl font-bold border-b pb-3 mb-4">Stream Chat Console</h2>

      {/* Message History Container */}
      <div className="flex-1 overflow-y-auto space-y-6 pr-2">
        {messages.map((message) => (
          <div key={message.id} className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`}>
            <div className={`p-4 rounded-2xl text-sm leading-relaxed max-w-xl ${message.role === "user" ? "bg-indigo-600 text-white" : "bg-gray-50 text-gray-800 border"}`}>
              <span className="font-semibold block text-xs uppercase opacity-75 mb-2">
                {message.role === "user" ? "You" : "AI Assistant"}
              </span>

              {/* Render Markdown with GFM plugins */}
              <ReactMarkdown
                remarkPlugins={[remarkGfm]}
                components={{
                  code({ node, className, children, ...props }) {
                    const match = /language-(\w+)/.exec(className || "");
                    const rawCodeText = String(children).replace(/\n$/, "");
                    
                    return match ? (
                      <div className="relative my-2 rounded-lg bg-gray-950 text-gray-200 p-4 overflow-x-auto text-xs">
                        <button
                          onClick={() => copyToClipboard(rawCodeText)}
                          className="absolute right-2 top-2 bg-gray-800 text-gray-400 hover:text-white px-2 py-1 rounded text-[10px]"
                        >
                          Copy
                        </button>
                        <pre className="font-mono">{rawCodeText}</pre>
                      </div>
                    ) : (
                      <code className="bg-gray-200 px-1 py-0.5 rounded font-mono text-xs" {...props}>
                        {children}
                      </code>
                    );
                  }
                }}
              >
                {message.content}
              </ReactMarkdown>
            </div>
          </div>
        ))}
      </div>

      {/* Input panel form */}
      <form onSubmit={handleSubmit} className="mt-4 flex gap-2 border-t pt-4">
        <input
          value={input}
          onChange={handleInputChange}
          className="flex-1 border px-4 py-3 rounded-xl text-sm focus:outline-none focus:border-indigo-600"
          placeholder="Ask for sample code structures..."
        />
        <button type="submit" className="bg-indigo-600 hover:bg-indigo-700 text-white font-semibold px-6 py-3 rounded-xl text-sm transition">
          Submit
        </button>
      </form>
    </div>
  );
}
Published on Last updated: