Skip to main content

Overview

The ADK Utils Example features a fully-functional chat interface built with modular React components. The UI provides a seamless conversational experience with streaming responses, markdown rendering, and syntax highlighting.

Streaming Support

Real-time message streaming with typing indicators

Markdown Rendering

Rich text formatting with Streamdown library

Syntax Highlighting

Beautiful code blocks with Vitesse themes

Mermaid Diagrams

Interactive diagram rendering in chat

Component Architecture

The chat interface is composed of five main components:
1

ChatHeader

Displays agent branding, message count, and quick action suggestions
2

ChatMessage

Renders individual messages with markdown and tool execution results
3

ChatInput

Handles user input with auto-resize and keyboard shortcuts
4

ChatEmptyState

Shows welcome message and prompt suggestions when chat is empty
5

ChatTypingIndicator

Animated dots indicating agent is processing

ChatHeader Component

The header provides navigation and quick access to common actions.
components/chat-header.tsx
"use client";

import { Bot } from "lucide-react";
import { Button } from "@/components/ui/button";
import { suggestions } from "@/lib/constants";

interface ChatHeaderProps {
  messageCount: number;
  onSuggestionClick: (text: string) => void;
  onReset: () => void;
}

export function ChatHeader({
  messageCount,
  onSuggestionClick,
  onReset,
}: ChatHeaderProps) {
  return (
    <header className="relative flex items-center justify-between bg-blue-600 text-white px-6 py-3">
      <button
        type="button"
        onClick={onReset}
        disabled={messageCount === 0}
        className={`flex items-center gap-3 py-1.5 rounded-lg transition-all ${
          messageCount > 0
            ? "hover:bg-white/20 hover:text-white cursor-pointer"
            : "cursor-default"
        }`}
      >
        <div className="flex h-9 w-9 items-center justify-center rounded-lg bg-accent">
          <Bot className="h-5 w-5 text-accent-foreground" />
        </div>
        <div>
          <h1 className="text-sm font-semibold">ADK Agent</h1>
          <p className="text-xs text-muted-foreground">Powered by Ollama Cloud</p>
        </div>
      </button>

      {messageCount > 0 && (
        <span className="rounded-full bg-muted px-3 py-1 text-xs">
          {messageCount} {messageCount === 1 ? "message" : "messages"}
        </span>
      )}
    </header>
  );
}
The header dynamically shows suggestion buttons after the first message is sent, providing contextual quick actions.

ChatMessage Component

The message component handles rendering of both user and agent messages with full markdown support.
components/chat-message.tsx
"use client";

import type { UIMessage } from "ai";
import { Bot, User } from "lucide-react";
import { Streamdown } from "streamdown";
import { createCodePlugin } from "@streamdown/code";
import { createMermaidPlugin } from "@streamdown/mermaid";

const code = createCodePlugin({
  themes: ["vitesse-light", "vitesse-dark"],
});

const mermaid = createMermaidPlugin({
  config: {
    startOnLoad: false,
    theme: "base",
    themeVariables: {
      darkMode: true,
      background: "#282a36",
      primaryColor: "#44475a",
      primaryTextColor: "#f8f8f2",
      primaryBorderColor: "#bd93f9",
    },
  },
});

interface ChatMessageProps {
  message: UIMessage;
  isLastBotMessage?: boolean;
}

export function ChatMessage({ message, isLastBotMessage = false }: ChatMessageProps) {
  const isUser = message.role === "user";

  return (
    <div className={`flex gap-3 ${isUser ? "flex-row-reverse" : "flex-row"}`}>
      <div className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg`}>
        {isUser ? <User className="h-4 w-4" /> : <Bot className="h-4 w-4" />}
      </div>
      <div className="flex flex-col gap-1">
        <span className="text-xs text-muted-foreground">
          {isUser ? "You" : "Agent"}
        </span>
        <div className="rounded-2xl px-4 py-3 text-sm">
          {message.parts.map((part, index) => {
            if (part.type === "text") {
              return (
                <div key={index} className="streamdown-content">
                  <Streamdown plugins={{ code, mermaid }}>
                    {part.text}
                  </Streamdown>
                </div>
              );
            }
            if (part.type.startsWith("tool-")) {
              const toolName = part.type.slice(5);
              const output = "output" in part ? part.output : null;

              return (
                <div key={index} className="mt-2 rounded-lg bg-muted px-3 py-2">
                  <span className="font-semibold">Tool: </span>
                  {toolName}
                  {output && <div className="mt-1">{JSON.stringify(output, null, 2)}</div>}
                </div>
              );
            }
            return null;
          })}
        </div>
      </div>
    </div>
  );
}
The ChatMessage component uses Streamdown with plugins for code syntax highlighting and Mermaid diagram rendering, providing rich content display capabilities.

Streaming Responses

Messages are rendered in real-time as they stream from the agent:
app/page.tsx
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";

const transport = new DefaultChatTransport({ api: "/api/genai-agent" });

export default function Home() {
  const { messages, sendMessage, status } = useChat({ transport });
  const isLoading = status === "streaming" || status === "submitted";

  return (
    <div className="flex flex-col">
      {messages.map((message) => (
        <ChatMessage key={message.id} message={message} />
      ))}
      {isLoading && <ChatTypingIndicator />}
    </div>
  );
}

ChatInput Component

The input component features auto-resizing textarea and keyboard shortcuts.
components/chat-input.tsx
"use client";

import { ArrowRight, Home } from "lucide-react";
import { useFocusOnLoad } from "@/hooks/use-focus-on-load";

interface ChatInputProps {
  input: string;
  onInputChange: (value: string) => void;
  onSubmit: () => void;
  isLoading: boolean;
}

export function ChatInput({
  input,
  onInputChange,
  onSubmit,
  isLoading,
}: ChatInputProps) {
  const textareaRef = useFocusOnLoad(isLoading);

  return (
    <div className="border-t bg-card px-6 py-4">
      <form onSubmit={(e) => { e.preventDefault(); onSubmit(); }}>
        <textarea
          ref={textareaRef}
          value={input}
          onChange={(e) => onInputChange(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === "Enter" && !e.shiftKey) {
              e.preventDefault();
              onSubmit();
            }
          }}
          placeholder="Ask the agent..."
          disabled={isLoading}
          className="w-full resize-none rounded-xl border px-4 py-3"
          style={{ minHeight: "48px", maxHeight: "160px" }}
          onInput={(e) => {
            const target = e.target as HTMLTextAreaElement;
            target.style.height = "auto";
            target.style.height = `${Math.min(target.scrollHeight, 160)}px`;
          }}
        />
        <button type="submit" disabled={!input.trim() || isLoading}>
          <ArrowRight className="h-4 w-4" />
        </button>
      </form>
    </div>
  );
}
Press Enter to send messages, Shift+Enter for new lines. The textarea automatically resizes up to 160px height.

ChatEmptyState Component

Shows a welcoming interface with prompt suggestions when the conversation is empty.
components/chat-empty-state.tsx
"use client";

import { Bot } from "lucide-react";
import { suggestions } from "@/lib/constants";

interface ChatEmptyStateProps {
  onSuggestionClick: (text: string) => void;
}

export function ChatEmptyState({ onSuggestionClick }: ChatEmptyStateProps) {
  return (
    <div className="flex flex-1 flex-col items-center justify-center gap-8 px-4">
      <div className="flex flex-col items-center gap-4">
        <div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-accent/10">
          <Bot className="h-8 w-8 text-accent" />
        </div>
        <h2 className="text-md text-foreground text-balance">
          Demonstration of the @yagolopez/adk-utils npm package
        </h2>
      </div>

      <div className="grid w-full max-w-md grid-cols-3 gap-3">
        {suggestions.map((item) => (
          <button
            key={item.label}
            onClick={() => onSuggestionClick(item.prompt)}
            className="flex flex-col items-center gap-2 rounded-xl border p-4"
          >
            <item.icon className="h-5 w-5" />
            <span className="text-xs font-medium">{item.label}</span>
          </button>
        ))}
      </div>
    </div>
  );
}

ChatTypingIndicator Component

A simple animated indicator shown while the agent is processing.
components/chat-typing-indicator.tsx
"use client";

export function ChatTypingIndicator() {
  return (
    <div className="flex items-center gap-1">
      <span className="h-2 w-2 rounded-full bg-muted-foreground animate-blink" 
            style={{animationDelay: "0ms"}}/>
      <span className="h-2 w-2 rounded-full bg-muted-foreground animate-blink" 
            style={{animationDelay: "200ms"}}/>
      <span className="h-2 w-2 rounded-full bg-muted-foreground animate-blink" 
            style={{animationDelay: "400ms"}}/>
    </div>
  );
}

Markdown Rendering with Streamdown

Streamdown provides real-time markdown parsing optimized for streaming content:

Code Syntax Highlighting

Vitesse light/dark themes via @streamdown/code

Mermaid Diagrams

Interactive diagrams via @streamdown/mermaid

Streaming Optimized

Renders content as it streams in

Markdown Support

Full CommonMark specification support

Auto-Scroll Behavior

The chat automatically scrolls to the bottom when new messages arrive:
hooks/use-scroll-to-bottom.ts
import { useEffect, RefObject } from "react";

export function useScrollToBottom(
  ref: RefObject<HTMLDivElement | null>,
  deps: unknown[]
) {
  useEffect(() => {
    if (ref.current) {
      ref.current.scrollTop = ref.current.scrollHeight;
    }
  }, [ref, ...deps]);
}
The scroll behavior triggers whenever messages or streaming status changes, ensuring the latest content is always visible.

Next Steps

Agent Tools

Learn about the available agent tools

Mermaid Diagrams

Explore diagram capabilities

Rate Limiting

Understand resource protection

Quickstart

Start building your own chat app

Build docs developers (and LLMs) love