Skip to content

Instantly share code, notes, and snippets.

@eimg
Last active August 9, 2026 15:54
Show Gist options
  • Select an option

  • Save eimg/f374804f3b68de1e3d3e82f6bae6e52e to your computer and use it in GitHub Desktop.

Select an option

Save eimg/f374804f3b68de1e3d3e82f6bae6e52e to your computer and use it in GitHub Desktop.
AI SDK အသုံးပြု၍ Web UI တည်ဆောက်ခြင်း - Tutorial

AI Agent နှင့် Web UI တည်ဆောက်ခြင်း

ပြီးခဲ့တဲ့ သင်ခန်းစာမှာ TypeScript, Vercel AI SDK နဲ့ OpenRouter တို့ကို အသုံးပြုပြီး AI agent တစ်ခုကို တည်ဆောက်ခဲ့ကြပါတယ်။

ကျွန်တော်တို့ရဲ့ Agent က -

  1. Language model ခေါ်ယူခြင်း
  2. Tool များအသုံးပြုခြင်း
  3. Agent Loop လုပ်ဆောင်ခြင်း
  4. Memory ယာယီသိမ်းဆည်းခြင်း
  5. Subagent များအသုံးပြုခြင်း
  6. Sequential Tasks များလုပ်ဆောင်ခြင်း
  7. Parallel Tasks များလုပ်ဆောင်ခြင်း

တို့ကို လုပ်ဆောင်နိုင်ခဲ့ပါတယ်။

ဒါပေမဲ့ အဲ့ဒီ Agent ဟာ Terminal ထဲမှာပဲ အလုပ်လုပ်တာ ဖြစ်ပါတယ်။

ဒီ သင်ခန်းစာမှာတော့ အဲ့ဒီ Agent ကို Web App အသေးလေးတစ်ခုအဖြစ် ပြောင်းလဲတည်ဆောက်သွားမှာ ဖြစ်ပါတယ်။

၁။ ရည်ရွယ်ချက်

သင်ခန်းစာ ပြီးဆုံးသွားတဲ့အခါ ကျွန်တော်တို့ရဲ့ App ဟာ -

  1. Streaming responses
  2. React Chat interface
  3. Conversation history
  4. Multiple chat sessions
  5. Old conversations
  6. Creating new conversations
  7. Basic state management
  8. Better error handling
  9. Stopping a streaming response

စတဲ့ အခြေခံလုပ်ဆောင်ချက် ပြည့်ပြည့်စုံစုံကို ရရှိသွားမှာ ဖြစ်ပါတယ်။

အသုံးပြုသွားမယ့် နည်းပညာတွေကတော့ အရင်အတိုင်းပဲ -

  • TypeScript
  • npm
  • Vercel AI SDK
  • OpenRouter
  • Zod

Web APP အတွက်တော့ Next.js ကို အသုံးပြုသွားမှာပါ။

Database တွေဘာတွေတော့ မသုံးသေးဘဲ Conversation တွေကို JSON ဖိုင်လေးတွေအနေနဲ့ပဲ သိမ်းဆည်းသွားပါမယ်။


၂။ ဘာတွေ တည်ဆောက်ကြမလဲ?

အရင်တုန်းက Architecture က ဒီလိုပါ -

User
  ↓
Terminal
  ↓
Agent
  ↓
OpenRouter
  ↓
Language model

အသစ်ပြောင်းလဲမယ့် Architecture ကတော့ ဒီလို ဖြစ်သွားပါမယ် -

Browser
  ↓
React chat interface
  ↓
Next.js API route
  ↓
AI agent
  ↓
OpenRouter
  ↓
Language model

File storage လည်း ထည့်သွင်းပါမယ် -

Browser
  ↓
Chat API
  ↓
Agent
  ↓
JSON conversation files

Language model က JSON ဖိုင်တွေကို တိုက်ရိုက်ဖတ်တာ ရေးတာ မလုပ်ပါဘူး။ ကျွန်တော်တို့ရဲ့ TypeScript ကုဒ်ကပဲ သိမ်းဆည်းတဲ့အပိုင်းကို လုပ်သွားမှာပါ။


၃။ Next.js Project

Next.js application အသစ်တစ်ခုကို အခုလို တည်ဆောက်ပါ -

npx create-next-app@latest my-agent-web

Setup လုပ်တဲ့အခါ မေးခွန်းတွေ မေးလာရင် အောက်ပါအတိုင်း ရွေးချယ်ပေးပါ -

Would you like to use the recommended Next.js defaults? 

Yes, use recommended defaults

ပြီးရင် Project directory ထဲကို ဝင်ပါ -

cd my-agent-web

လိုအပ်တဲ့ AI packages တွေကို Install လုပ်ပါ -

npm install ai @ai-sdk/react @openrouter/ai-sdk-provider zod

ဒီ Packages တွေရဲ့ အဓိကလုပ်ဆောင်ချက်တွေကတော့ -

  • ai : Vercel AI SDK ရဲ့ ပင်မ Package ဖြစ်ပါတယ်။
  • @ai-sdk/react : useChat() လိုမျိုး React hooks တွေ ပါဝင်ပါတယ်။
  • @openrouter/ai-sdk-provider : AI SDK နဲ့ OpenRouter ကို ချိတ်ဆက်ပေးပါတယ်။
  • zod : Tool input တွေကို စစ်ဆေးဖို့ အသုံးပြုပါတယ်။

၄။ Environment Variables

Project ရဲ့ အပြင်ဘက်ဆုံး (Root) မှာ .env.local ဖိုင်တစ်ခု ဖန်တီးပြီး အောက်ပါအတိုင်း ရေးထည့်ပါ -

OPENROUTER_API_KEY=your-openrouter-api-key
OPENROUTER_MODEL=xiaomi/mimo-v2.5

OpenRouter Model ကို မိမိနှစ်သက်ရာ အခြား Model နဲ့လည်း အစားထိုးနိုင်ပါတယ်။ နောက်ပိုင်း Calculator tool ကို အသုံးပြုမှာဖြစ်လို့ ရွေးချယ်လိုက်တဲ့ Model က Tool calling ရဖို့တော့ လိုပါတယ်။

API Key ကို Client-side React ကုဒ်တွေထဲမှာ မထည့်မိဖို့ကို သတိပြုပါ။ Server-side ဘက်မှာပဲ ရှိရပါမယ်။


၅။ Folder Structure

ကျွန်တော်တို့ တဖြည်းဖြည်းချင်း တည်ဆောက်သွားမယ့် Folder Structure က အောက်ပါအတိုင်း ဖြစ်ပါတယ် -

my-agent-web/
├── app/
│   ├── api/
│   │   ├── chat/
│   │   │   └── route.ts
│   │   └── chats/
│   │       └── route.ts
│   ├── chat/
│   │   ├── [id]/
│   │   │   └── page.tsx
│   │   └── page.tsx
│   ├── globals.css
│   ├── layout.tsx
│   └── page.tsx
├── components/
│   ├── chat-app.tsx
│   ├── chat-input.tsx
│   ├── chat-sidebar.tsx
│   └── message-list.tsx
├── lib/
│   ├── agent.ts
│   ├── chat-store.ts
│   ├── message-utils.ts
│   └── tools.ts
├── scripts/
│   └── stream-test.ts
├── .data/
│   └── chats/
├── .env.local
├── package.json
└── tsconfig.json

.data/chats folder ဟာ ပထမဆုံး Chat History ကို သိမ်းဆည်းတဲ့အခါ အလိုအလျောက် ဖန်တီးသွားမှာပါ။


၆။ Streaming

ပြီးခဲ့တဲ့ သင်ခန်းစာမှာ generateText() ကို အသုံးပြုခဲ့ပါတယ် -

const result = await generateText({
  model,
  prompt: "Explain AI agents.",
});

console.log(result.text);

generateText() ဟာ စာတွေ အကုန်လုံး ထုတ်ပေးလို့ ပြီးတဲ့အထိ စောင့်ပါတယ်။

Flow က ဒီလိုပါ -

Send prompt
  ↓
Wait for complete generation
  ↓
Receive complete answer
  ↓
Display answer

Streaming ရဲ့ အလုပ်လုပ်ပုံကတော့ ကွဲပြားပါတယ် -

Send prompt
  ↓
Receive first piece
  ↓
Display it
  ↓
Receive next piece
  ↓
Display it
  ↓
Continue until complete

Streaming ဟာ Model ကို ပိုမြန်မြန် စာထုတ်ပေးအောင် လုပ်တာမဟုတ်ပါဘူး။ User အနေနဲ့ အဖြေအပြည့်အစုံ မပြီးခင်မှာကတည်းက စာတွေကို တဖြည်းဖြည်းချင်း မြင်တွေ့နိုင်အောင် လုပ်ပေးတာ ဖြစ်ပါတယ်။


၇။ Streaming စမ်းသပ်ကြည့်ခြင်း

Web interface မတည်ဆောက်ခင် Streaming သီးသန့် အလုပ်လုပ်ပုံကို အရင်စမ်းကြည့်ရအောင်။

scripts/stream-test.ts ဆိုပြီး ဖိုင်တစ်ခု ဖန်တီးပါ (scripts folder မရှိသေးရင် အသစ်ဆောက်ပါ)။ ပြီးတဲ့အခါ ဒီကုဒ်ကို ထည့်ပါ -

import { createOpenRouter } from "@openrouter/ai-sdk-provider";
import { streamText } from "ai";

async function main() {
  const apiKey = process.env.OPENROUTER_API_KEY;
  const modelName = process.env.OPENROUTER_MODEL;

  if (!apiKey) {
    throw new Error("OPENROUTER_API_KEY is missing.");
  }

  if (!modelName) {
    throw new Error("OPENROUTER_MODEL is missing.");
  }

  const openrouter = createOpenRouter({
    apiKey,
  });

  const result = streamText({
    model: openrouter(modelName),
    prompt: "Explain streaming responses in simple terms.",
  });

  for await (const textPart of result.textStream) {
    process.stdout.write(textPart);
  }

  process.stdout.write("\n");
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

ဒီဖိုင်ကို သီးသန့် Run လို့ရအောင် tsx ကို install လုပ်ပါ -

npm install -D tsx

package.json ထဲက scripts မှာ stream-test ကို သွားထည့်ပေးပါ (ရှိပြီးသား Next.js script တွေကို မဖျက်ပါနဲ့) -

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "stream-test": "tsx --env-file=.env.local scripts/stream-test.ts"
  }
}

အခုလို Run ကြည့်ပါ -

npm run stream-test

စာတွေ တဖြည်းဖြည်းချင်း Stream အနေနဲ့ ထွက်လာတာကို တွေ့ရပါလိမ့်မယ်။

အဓိကကုဒ်ကတော့ streamText ကို အသုံးပြုထားတာပါပဲ -

const result = streamText({
  model,
  prompt,
});

ပြီးတော့ ရလာတဲ့ အပိုင်းအစလေးတွေကို အခုလို loop ပတ်ပြီး ထုတ်ယူပါတယ် -

for await (const textPart of result.textStream) {
  process.stdout.write(textPart);
}

၈။ OpenRouter Agent

Agent နဲ့သက်ဆိုင်တဲ့ Logic တွေကို API route ထဲမှာ အကုန်မရေးဘဲ သီးသန့်ခွဲထုတ်ပါမယ်။

lib/agent.ts ကို ဖန်တီးပြီး အောက်ပါအတိုင်း ရေးပါ -

import { createOpenRouter } from "@openrouter/ai-sdk-provider";

const apiKey = process.env.OPENROUTER_API_KEY;
const modelName = process.env.OPENROUTER_MODEL;

if (!apiKey) {
  throw new Error("OPENROUTER_API_KEY is missing.");
}

if (!modelName) {
  throw new Error("OPENROUTER_MODEL is missing.");
}

const openrouter = createOpenRouter({
  apiKey,
});

export const agentModel = openrouter(modelName);

export const agentSystemPrompt = `
You are a helpful AI assistant.

Explain ideas clearly and directly.
Use the calculator tool whenever accurate arithmetic is required.
After using a tool, explain the result to the user.

Do not claim that you performed an action unless a tool actually performed it.
`;

ဒီ module ထဲမှာ -

  • Provider သတ်မှတ်ခြင်း
  • Model ရွေးချယ်ခြင်း
  • System instructions များ ပါဝင်ပါတယ်။

UI တွေ၊ HTTP request တွေ၊ သိမ်းဆည်းတဲ့အပိုင်းတွေကို ဒီထဲမှာ မထည့်ပါဘူး။ သီးခြားခွဲထားမှာပါ။


၉။ Calculator Tool

lib/tools.ts ကို ဖန်တီးပြီး အောက်ပါအတိုင်း ရေးပါ -

import { tool } from "ai";
import { z } from "zod";

export const calculatorTool = tool({
  description:
    "Perform accurate arithmetic using two numbers. Use this tool when arithmetic is required.",

  inputSchema: z.object({
    operation: z
      .enum(["add", "subtract", "multiply", "divide"])
      .describe("The arithmetic operation to perform"),

    a: z.number().describe("The first number"),

    b: z.number().describe("The second number"),
  }),

  execute: async ({ operation, a, b }) => {
    switch (operation) {
      case "add":
        return {
          success: true,
          result: a + b,
        };

      case "subtract":
        return {
          success: true,
          result: a - b,
        };

      case "multiply":
        return {
          success: true,
          result: a * b,
        };

      case "divide":
        if (b === 0) {
          return {
            success: false,
            error: "Division by zero is not allowed.",
          };
        }

        return {
          success: true,
          result: a / b,
        };
    }
  },
});

export const agentTools = {
  calculator: calculatorTool,
};

Tools တွေကို Object တစ်ခုတည်းအနေနဲ့ ပေါင်းပြီး Export လုပ်ထားပါတယ် -

export const agentTools = {
  calculator: calculatorTool,
};

ဒီလိုလုပ်ထားတာက streamText() မှာပဲဖြစ်ဖြစ်၊ Message တွေ Validate လုပ်တဲ့အခါမှာပဲဖြစ်ဖြစ် အလွယ်တကူ ထည့်သွင်းအသုံးပြုနိုင်ဖို့ ဖြစ်ပါတယ်။


၁၀။ Chat API

app/api/chat/route.ts ကို ဖန်တီးပြီး အောက်ပါအတိုင်း ရေးပါ -

import {
  convertToModelMessages,
  createIdGenerator,
  type InferUITools,
  stepCountIs,
  streamText,
  type UIDataTypes,
  type UIMessage,
  validateUIMessages,
} from "ai";

import {
  agentModel,
  agentSystemPrompt,
} from "@/lib/agent";

import { agentTools } from "@/lib/tools";

export const runtime = "nodejs";

type AgentUIMessage = UIMessage<
  never,
  UIDataTypes,
  InferUITools<typeof agentTools>
>;

type ChatRequestBody = {
  id?: string;
  messages?: AgentUIMessage[];
};

export async function POST(request: Request): Promise<Response> {
  try {
    const body = (await request.json()) as ChatRequestBody;

    if (!body.id) {
      return Response.json(
        {
          error: "Chat ID is required.",
        },
        {
          status: 400,
        },
      );
    }

    if (!Array.isArray(body.messages)) {
      return Response.json(
        {
          error: "Messages must be an array.",
        },
        {
          status: 400,
        },
      );
    }

    const validatedMessages = await validateUIMessages<AgentUIMessage>({
      messages: body.messages,
      tools: agentTools,
    });

    const modelMessages =
      await convertToModelMessages(validatedMessages);

    const result = streamText({
      model: agentModel,

      system: agentSystemPrompt,

      messages: modelMessages,

      tools: agentTools,

      stopWhen: stepCountIs(5),
    });

    return result.toUIMessageStreamResponse({
      originalMessages: validatedMessages,

      generateMessageId: createIdGenerator({
        prefix: "msg",
        size: 16,
      }),

      onError: (error) => {
        console.error("Streaming error:", error);

        return "The assistant could not complete the response.";
      },
    });
  } catch (error) {
    console.error("Chat request failed:", error);

    return Response.json(
      {
        error: "The chat request failed.",
      },
      {
        status: 500,
      },
    );
  }
}

အခုလောလောဆယ်တော့ History သိမ်းဆည်းတဲ့ အပိုင်းကို မထည့်ရသေးပါဘူး။

ဒီ API endpoint က -

  1. Message တွေကို လက်ခံတယ်
  2. မှန်မမှန် စစ်ဆေးတယ်
  3. UI message ကနေ Model message ကို ပြောင်းပေးတယ်
  4. Model ကို လှမ်းခေါ်တယ်
  5. Tool တွေ လိုအပ်ရင် run ပေးတယ်
  6. Browser ဆီကို Response ကို Streaming ပုံစံနဲ့ ပြန်ပို့ပေးပါတယ်။

၁၁။ UI Messages နှင့် Model Messages

Browser နဲ့ Model ဟာ Message format အမျိုးအစား မတူညီတာကို သုံးကြပါတယ်။

React UI က UIMessage[] ကို သုံးပါတယ် -

type UIMessage = {
  id: string;
  role: "user" | "assistant" | "system";
  parts: UIMessagePart[];
};

Model ကတော့ ModelMessage[] ကို သုံးပါတယ်။ ဒါကြောင့် convertToModelMessages နဲ့ ပြောင်းလဲပေးရတာပါ -

const modelMessages = await convertToModelMessages(validatedMessages);

Flow က ဒီလိုပါ -

Browser UIMessage[]
  ↓
convertToModelMessages()
  ↓
ModelMessage[]
  ↓
Language model

နောက်ပိုင်း သိမ်းဆည်းတဲ့အခါမှာ UI ဖက်က လိုအပ်တဲ့ ဖွဲ့စည်းပုံတွေ ပါဝင်တဲ့ UIMessage တွေကိုပဲ အဓိကထား သိမ်းဆည်းသွားမှာပါ။


၁၂။ Message Validation

Browser ကနေ Server ဆီ Data တွေ ပို့တဲ့အခါ မှန်ကန်တယ်လို့ အမြဲမယူဆသင့်ပါဘူး။ ဒီကုဒ်က ဝင်လာတဲ့ Message တွေကို စစ်ဆေးပေးပါတယ် -

const validatedMessages = await validateUIMessages<AgentUIMessage>({
  messages: body.messages,
  tools: agentTools,
});

Tool calls တွေ၊ Tool results တွေ၊ Metadata တွေ ပါလာတဲ့အခါ အမှားအယွင်းမရှိအောင် Validate လုပ်ပေးခြင်းက Tool information အမှားတွေ Model ဆီ ရောက်မသွားအောင် ကာကွယ်ပေးပါတယ်။


၁၃။ toUIMessageStreamResponse()

streamText() ကနေ ပြန်ရလာတဲ့ Result ဟာ ရိုးရိုး JSON object မဟုတ်ပါဘူး။ Active stream တစ်ခု ဖြစ်ပါတယ်။ ဒါကို useChat() က နားလည်တဲ့ HTTP response အဖြစ် ပြောင်းလဲပေးဖို့ ဒီ method ကို သုံးရတာပါ -

return result.toUIMessageStreamResponse();

၁၄။ Chat Component

components/chat-app.tsx ကို ဖန်တီးပြီး အောက်ပါအတိုင်း ရေးပါ -

"use client";

import { useChat } from "@ai-sdk/react";
import {
  DefaultChatTransport,
  type UIMessage,
} from "ai";

import {
  type SubmitEvent,
  useState,
} from "react";

type ChatAppProps = {
  chatId: string;
  initialMessages?: UIMessage[];
};

export function ChatApp({
  chatId,
  initialMessages = [],
}: ChatAppProps) {
  const [input, setInput] = useState("");

  const {
    messages,
    sendMessage,
    status,
    error,
    stop,
  } = useChat({
    id: chatId,

    messages: initialMessages,

    transport: new DefaultChatTransport({
      api: "/api/chat",
    }),

    onError: (chatError) => {
      console.error("Chat error:", chatError);
    },
  });

  const isBusy =
    status === "submitted" ||
    status === "streaming";

  async function handleSubmit(
    event: SubmitEvent<HTMLFormElement>,
  ): Promise<void> {
    event.preventDefault();

    const message = input.trim();

    if (!message || isBusy) {
      return;
    }

    setInput("");

    await sendMessage({
      text: message,
    });
  }

  return (
    <main className="chat-page">
      <section className="chat-panel">
        <header className="chat-header">
          <div>
            <h1>My AI Agent</h1>
            <p>Chat ID: {chatId}</p>
          </div>

          {isBusy && (
            <button
              type="button"
              className="secondary-button"
              onClick={stop}
            >
              Stop
            </button>
          )}
        </header>

        <section className="message-list">
          {messages.length === 0 && (
            <div className="empty-chat">
              <h2>Start a conversation</h2>
              <p>
                Ask a question or request a calculation.
              </p>
            </div>
          )}

          {messages.map((message) => (
            <article
              key={message.id}
              className={`message message-${message.role}`}
            >
              <strong>
                {message.role === "user"
                  ? "You"
                  : "Assistant"}
              </strong>

              <div className="message-content">
                {message.parts.map((part, index) => {
                  if (part.type === "text") {
                    return (
                      <p key={`${message.id}-${index}`}>
                        {part.text}
                      </p>
                    );
                  }

                  return null;
                })}
              </div>
            </article>
          ))}

          {status === "submitted" && (
            <div className="status-message">
              Assistant is thinking...
            </div>
          )}
        </section>

        {error && (
          <div className="error-message">
            {error.message ||
              "Something went wrong while generating the response."}
          </div>
        )}

        <form
          className="chat-form"
          onSubmit={handleSubmit}
        >
          <textarea
            value={input}
            onChange={(event) =>
              setInput(event.target.value)
            }
            placeholder="Type your message..."
            rows={3}
            disabled={isBusy}
          />

          <button
            type="submit"
            disabled={!input.trim() || isBusy}
          >
            Send
          </button>
        </form>
      </section>
    </main>
  );
}

၁၅။ useChat()

အဓိက အစိတ်အပိုင်းကတော့ ဒါပါ -

const {
  messages,
  sendMessage,
  status,
  error,
  stop,
} = useChat({ ... });

useChat() က Client-side ရဲ့ Chat state ကို စီမံပေးပါတယ်။

  • messages: လက်ရှိ ပြသပေးနေတဲ့ messages တွေ
  • sendMessage: User ရဲ့ Message ကို ပို့ပေးမယ့် Function
  • status: လက်ရှိ Request ရဲ့ အခြေအနေ
  • error: Error တက်ရင် ပြပေးမယ့် အချက်အလက်
  • stop: အလုပ်လုပ်နေတဲ့ Stream ကို ရပ်ပစ်မယ့် Function

၁၆။ Chat Status

Status တွေက အောက်ပါအတိုင်း ဖြစ်နိုင်ပါတယ် -

  • ready: နောက်ထပ် Message လက်ခံဖို့ အသင့်ဖြစ်နေတဲ့ အချိန်
  • submitted: Request ပို့ပြီးပြီ၊ ဒါပေမဲ့ Response ပြန်မလာသေးတဲ့ အချိန်
  • streaming: Response တဖြည်းဖြည်းချင်း ဝင်လာနေတဲ့ အချိန်
  • error: Request ပို့တာ Fail ဖြစ်သွားတဲ့ အချိန်

၁၇။ Message Parts များကို ဖော်ပြခြင်း

UI Message တစ်ခုမှာ စာသားတွေကို တိုက်ရိုက်သိမ်းထားတာ မဟုတ်ဘဲ parts အနေနဲ့ သိမ်းထားတာပါ -

{message.parts.map((part, index) => {
  if (part.type === "text") {
    return (
      <p key={`${message.id}-${index}`}>
        {part.text}
      </p>
    );
  }
  return null;
})}

နောက်ပိုင်းမှာ Tool calls တွေ၊ Results တွေလည်း ဒီ parts ထဲမှာ ပါလာမှာ ဖြစ်ပေမဲ့ အခုလောလောဆယ်တော့ Text ကိုပဲ ပြပေးထားပါတယ်။


၁၈။ ယာယီ Chat Page တည်ဆောက်ခြင်း

app/page.tsx ကို ဖွင့်ပြီး အောက်ပါအတိုင်း အစားထိုးပါ -

import { ChatApp } from "@/components/chat-app";

export default function HomePage() {
  return (
    <ChatApp
      chatId="temporary-chat"
      initialMessages={[]}
    />
  );
}

အခုချိန်မှာ Persistence တို့၊ Multiple chats တို့ မပါသေးပါဘူး။ Streaming ကို စမ်းဖို့အတွက်ပဲ ဖြစ်ပါတယ်။


၁၉။ အခြေခံ CSS ထည့်သွင်းခြင်း

app/globals.css ထဲက စာတွေကို အောက်ပါကုဒ်နဲ့ အစားထိုးလိုက်ပါ -

* { box-sizing: border-box; }
html, body { margin: 0; min-height: 100%; }
body { background: #f4f5f7; color: #202124; font-family: Arial, Helvetica, sans-serif; }
button, textarea { font: inherit; }
button { cursor: pointer; }
button:disabled { cursor: not-allowed; opacity: 0.6; }
.chat-page { min-height: 100vh; padding: 24px; }
.chat-panel { display: flex; flex-direction: column; width: min(900px, 100%); height: calc(100vh - 48px); margin: 0 auto; overflow: hidden; background: white; border: 1px solid #dfe1e5; border-radius: 12px; }
.chat-header { display: flex; align-items: center; justify-content: space-between; padding: 18px 20px; border-bottom: 1px solid #e5e7eb; }
.chat-header h1 { margin: 0; font-size: 20px; }
.chat-header p { margin: 4px 0 0; color: #6b7280; font-size: 12px; }
.message-list { flex: 1; overflow-y: auto; padding: 24px; }
.empty-chat { max-width: 480px; margin: 100px auto; text-align: center; }
.empty-chat h2 { margin-bottom: 8px; }
.empty-chat p { color: #6b7280; }
.message { max-width: 75%; margin-bottom: 18px; padding: 14px 16px; border-radius: 12px; line-height: 1.55; }
.message strong { display: block; margin-bottom: 6px; font-size: 13px; }
.message-user { margin-left: auto; background: #e8f0fe; }
.message-assistant { margin-right: auto; background: #f1f3f4; }
.message-content p { margin: 0; white-space: pre-wrap; }
.status-message { color: #6b7280; font-size: 14px; }
.error-message { margin: 0 20px 12px; padding: 12px; color: #991b1b; background: #fee2e2; border-radius: 8px; }
.chat-form { display: flex; gap: 12px; padding: 16px; border-top: 1px solid #e5e7eb; }
.chat-form textarea { flex: 1; resize: none; padding: 12px; border: 1px solid #cbd5e1; border-radius: 8px; }
.chat-form button, .secondary-button { padding: 10px 18px; color: white; background: #1f2937; border: 0; border-radius: 8px; }
.secondary-button { background: #64748b; }

၂၀။ Application ကို Run ကြည့်ခြင်း

npm run dev

http://localhost:3000 ကို ဖွင့်ပြီး စမ်းသပ်ကြည့်ပါ။

"Explain what an AI agent is." လို့ ရိုက်ထည့်ကြည့်ပါ၊ အဖြေတွေ တဖြည်းဖြည်း ထွက်လာတာ တွေ့ရပါမယ်။

ပြီးရင် "Calculate 48 multiplied by 17." လို့ ရိုက်ကြည့်ပါ၊ Tool ကို လှမ်းသုံးသွားတာကိုပါ တွေ့ရမှာပါ။

လက်ရှိ Flow က ဒီလို အလုပ်လုပ်နေတာပါ -

User types message
  ↓
useChat() sends UI messages
  ↓
POST /api/chat
  ↓
Messages are validated and converted
  ↓
streamText() calls the model
  ↓
Tool may execute
  ↓
Response streams to the browser
  ↓
useChat() updates the message
  ↓
React rerenders

၂၁။ Persistence

အခုချိန်မှာ Browser ကပဲ Message တွေကို မှတ်ထားပေးတာဖြစ်လို့ Page ကို Refresh လုပ်လိုက်ရင် ပျောက်သွားမှာပါ။

ဒါကြောင့် Chat တွေကို JSON ဖိုင်တွေအနေနဲ့ .data/chats/ အောက်မှာ သိမ်းဆည်းပေးမယ့်စနစ် လိုအပ်ပါတယ်။ Database သုံးမယ့်အစား အလွယ်တကူ လေ့လာနိုင်ဖို့ File storage ကိုပဲ သုံးပါမယ်။

၂၂။ Storage အတွက် လိုအပ်သည်များ

lib/chat-store.ts ကို ဖန်တီးပြီး အောက်ပါအတိုင်း အပြည့်အစုံ ရေးထည့်ပါ -

import {
  createIdGenerator,
  type UIMessage,
} from "ai";

import {
  mkdir,
  readFile,
  readdir,
  rename,
  writeFile,
} from "node:fs/promises";

import path from "node:path";

export type StoredChat = {
  id: string;
  title: string;
  messages: UIMessage[];
  createdAt: string;
  updatedAt: string;
};

export type ChatSummary = {
  id: string;
  title: string;
  createdAt: string;
  updatedAt: string;
};

const createChatId = createIdGenerator({
  prefix: "chat",
  size: 16,
});

const chatDirectory = path.join(
  process.cwd(),
  ".data",
  "chats",
);

async function ensureChatDirectory(): Promise<void> {
  await mkdir(chatDirectory, {
    recursive: true,
  });
}

function getChatFilePath(chatId: string): string {
  if (!/^[a-zA-Z0-9_-]+$/.test(chatId)) {
    throw new Error("Invalid chat ID.");
  }

  return path.join(
    chatDirectory,
    `${chatId}.json`,
  );
}

function getTextFromMessage(
  message: UIMessage,
): string {
  return message.parts
    .filter(
      (
        part,
      ): part is Extract<
        UIMessage["parts"][number],
        { type: "text" }
      > => part.type === "text",
    )
    .map((part) => part.text)
    .join("")
    .trim();
}

function createChatTitle(
  messages: UIMessage[],
): string {
  const firstUserMessage = messages.find(
    (message) => message.role === "user",
  );

  if (!firstUserMessage) {
    return "New conversation";
  }

  const text = getTextFromMessage(
    firstUserMessage,
  );

  if (!text) {
    return "New conversation";
  }

  const maximumLength = 48;

  if (text.length <= maximumLength) {
    return text;
  }

  return `${text.slice(0, maximumLength)}…`;
}

async function writeJsonFile(
  filePath: string,
  value: unknown,
): Promise<void> {
  const temporaryPath = `${filePath}.tmp`;

  const json = JSON.stringify(
    value,
    null,
    2,
  );

  await writeFile(
    temporaryPath,
    json,
    "utf8",
  );

  await rename(
    temporaryPath,
    filePath,
  );
}

export async function createChat(): Promise<StoredChat> {
  await ensureChatDirectory();

  const now = new Date().toISOString();

  const chat: StoredChat = {
    id: createChatId(),
    title: "New conversation",
    messages: [],
    createdAt: now,
    updatedAt: now,
  };

  await writeJsonFile(
    getChatFilePath(chat.id),
    chat,
  );

  return chat;
}

export async function loadChat(
  chatId: string,
): Promise<StoredChat | null> {
  await ensureChatDirectory();

  try {
    const content = await readFile(
      getChatFilePath(chatId),
      "utf8",
    );

    return JSON.parse(content) as StoredChat;
  } catch (error) {
    const nodeError =
      error as NodeJS.ErrnoException;

    if (nodeError.code === "ENOENT") {
      return null;
    }

    throw error;
  }
}

export async function saveChat({
  chatId,
  messages,
}: {
  chatId: string;
  messages: UIMessage[];
}): Promise<StoredChat> {
  const existingChat =
    await loadChat(chatId);

  if (!existingChat) {
    throw new Error(
      `Chat "${chatId}" does not exist.`,
    );
  }

  const updatedChat: StoredChat = {
    ...existingChat,
    title: createChatTitle(messages),
    messages,
    updatedAt: new Date().toISOString(),
  };

  await writeJsonFile(
    getChatFilePath(chatId),
    updatedChat,
  );

  return updatedChat;
}

export async function listChats(): Promise<
  ChatSummary[]
> {
  await ensureChatDirectory();

  const filenames = await readdir(
    chatDirectory,
  );

  const chatFiles = filenames.filter(
    (filename) =>
      filename.endsWith(".json") &&
      !filename.endsWith(".tmp"),
  );

  const chats = await Promise.all(
    chatFiles.map(async (filename) => {
      try {
        const content = await readFile(
          path.join(chatDirectory, filename),
          "utf8",
        );

        const chat =
          JSON.parse(content) as StoredChat;

        return {
          id: chat.id,
          title: chat.title,
          createdAt: chat.createdAt,
          updatedAt: chat.updatedAt,
        } satisfies ChatSummary;
      } catch (error) {
        console.error(
          `Could not read ${filename}:`,
          error,
        );

        return null;
      }
    }),
  );

  return chats
    .filter(
      (
        chat,
      ): chat is ChatSummary =>
        chat !== null,
    )
    .sort(
      (first, second) =>
        new Date(second.updatedAt).getTime() -
        new Date(first.updatedAt).getTime(),
    );
}

ဒီဖိုင်ထဲမှာ Chat အသစ်တည်ဆောက်တာ၊ ဖတ်တာ၊ သိမ်းတာ၊ စာရင်းထုတ်တာတွေကို လုပ်ဆောင်ပေးမယ့် Function တွေ ပါဝင်ပါတယ်။ JSON ဖိုင်ကို Save တဲ့အခါ ယာယီ .tmp ဖိုင်တစ်ခု အရင်ရေးပြီးမှ အမည်ပြောင်းတဲ့နည်း (Safe Write) ကို အသုံးပြုထားပါတယ်။


၂၃။ Chat API ပြင်ဆင်ခြင်း

app/api/chat/route.ts ကို ဖွင့်ပြီး loadChat, saveChat ကို import လုပ်ကာ POST function ထဲက onFinish မှာ သိမ်းဆည်းမယ့် လုပ်ဆောင်ချက် ထည့်ပါမယ်။ ဖိုင်အပြည့်အစုံကို အောက်ပါအတိုင်း အစားထိုးလိုက်ပါ -

import {
  convertToModelMessages,
  createIdGenerator,
  type InferUITools,
  stepCountIs,
  streamText,
  type UIDataTypes,
  type UIMessage,
  validateUIMessages,
} from "ai";

import {
  agentModel,
  agentSystemPrompt,
} from "@/lib/agent";

import {
  loadChat,
  saveChat,
} from "@/lib/chat-store";

import { agentTools } from "@/lib/tools";

export const runtime = "nodejs";

type AgentUIMessage = UIMessage<
  never,
  UIDataTypes,
  InferUITools<typeof agentTools>
>;

type ChatRequestBody = {
  id?: string;
  messages?: AgentUIMessage[];
};

export async function POST(
  request: Request,
): Promise<Response> {
  try {
    const body =
      (await request.json()) as ChatRequestBody;

    if (!body.id) {
      return Response.json(
        { error: "Chat ID is required." },
        { status: 400 },
      );
    }

    if (!Array.isArray(body.messages)) {
      return Response.json(
        { error: "Messages must be an array." },
        { status: 400 },
      );
    }

    const storedChat = await loadChat(body.id);

    if (!storedChat) {
      return Response.json(
        { error: "Chat was not found." },
        { status: 404 },
      );
    }

    const validatedMessages = await validateUIMessages<AgentUIMessage>({
      messages: body.messages,
      tools: agentTools,
    });

    const modelMessages = await convertToModelMessages(validatedMessages);

    const result = streamText({
      model: agentModel,
      system: agentSystemPrompt,
      messages: modelMessages,
      tools: agentTools,
      stopWhen: stepCountIs(5),
    });

    return result.toUIMessageStreamResponse({
      originalMessages: validatedMessages,

      generateMessageId: createIdGenerator({
        prefix: "msg",
        size: 16,
      }),

      onFinish: async ({ messages }) => {
        try {
          await saveChat({
            chatId: body.id!,
            messages,
          });
        } catch (error) {
          console.error("Could not save chat:", error);
        }
      },

      onError: (error) => {
        console.error("Streaming error:", error);
        return "The assistant could not complete the response.";
      },
    });
  } catch (error) {
    console.error("Chat request failed:", error);
    return Response.json(
      { error: "The chat request failed." },
      { status: 500 },
    );
  }
}

onFinish ထဲမှာ ပါလာတဲ့ messages တွေက အစအဆုံး အကုန်ပါဝင်တဲ့အတွက် အဲ့ဒါကို တိုက်ရိုက် Save လုပ်လိုက်ရုံပါပဲ။ Streaming ပြီးတာနဲ့ သိမ်းလိုက်တဲ့ သဘောပါ။


၂၄။ New Chat Page

app/chat/page.tsx ဖိုင်ဖန်တီးပြီး အခုလို ရေးပါ -

import { redirect } from "next/navigation";
import { createChat } from "@/lib/chat-store";

export const dynamic = "force-dynamic";

export default async function NewChatPage() {
  const chat = await createChat();
  redirect(`/chat/${chat.id}`);
}

/chat ကို ဝင်လိုက်တာနဲ့ Chat အသစ်တစ်ခု ဆောက်ပြီး ချက်ချင်း Redirect လုပ်သွားမှာပါ။

၂၅။ Home Page ကို Redirect လုပ်ခြင်း

app/page.tsx ကို အခုလို ပြင်ပါ -

import { redirect } from "next/navigation";

export default function HomePage() {
  redirect("/chat");
}

၂၆။ Existing Chat Page

app/chat/[id]/page.tsx ကို ဖန်တီးပြီး အခုလို ရေးပါ -

import { notFound } from "next/navigation";
import { ChatApp } from "@/components/chat-app";
import {
  listChats,
  loadChat,
} from "@/lib/chat-store";

export const dynamic = "force-dynamic";

type ChatPageProps = {
  params: Promise<{
    id: string;
  }>;
};

export default async function ChatPage({
  params,
}: ChatPageProps) {
  const { id } = await params;

  const [chat, chats] = await Promise.all([
    loadChat(id),
    listChats(),
  ]);

  if (!chat) {
    notFound();
  }

  return (
    <ChatApp
      chatId={chat.id}
      initialMessages={chat.messages}
      initialChats={chats}
    />
  );
}

ဒီနေရာမှာ Promise.all ကိုသုံးပြီး လိုအပ်တဲ့ data နှစ်ခုကို Parallel ယူထားတာ သတိပြုပါ။


၂၇။ Chat Component ပြင်ဆင်ခြင်း

components/chat-app.tsx မှာ initialChats ထပ်ထည့်ပေးဖို့ လိုလာပါပြီ။ ခဏနေရင် Sidebar လည်း ထည့်ရမှာမို့ ChatApp တစ်ခုလုံးကို နောက်ပိုင်းကျရင် အပြည့်အစုံ အစားထိုးပါမယ်။

၂၈။ Sidebar Component

components/chat-sidebar.tsx ဖန်တီးပြီး အခုလို ရေးပါ -

"use client";

import Link from "next/link";
import type { ChatSummary } from "@/lib/chat-store";

type ChatSidebarProps = {
  activeChatId: string;
  chats: ChatSummary[];
};

export function ChatSidebar({
  activeChatId,
  chats,
}: ChatSidebarProps) {
  return (
    <aside className="chat-sidebar">
      <Link href="/chat" className="new-chat-button">
        + New chat
      </Link>

      <nav className="conversation-list">
        {chats.length === 0 && (
          <p className="no-conversations">
            No conversations yet.
          </p>
        )}

        {chats.map((chat) => (
          <Link
            key={chat.id}
            href={`/chat/${chat.id}`}
            className={
              chat.id === activeChatId
                ? "conversation-link conversation-link-active"
                : "conversation-link"
            }
          >
            <span className="conversation-title">{chat.title}</span>
            <span className="conversation-date">{formatDate(chat.updatedAt)}</span>
          </Link>
        ))}
      </nav>
    </aside>
  );
}

function formatDate(dateString: string): string {
  const date = new Date(dateString);
  return new Intl.DateTimeFormat(undefined, {
    month: "short",
    day: "numeric",
  }).format(date);
}

၂၉။ Message List ခွဲထုတ်ခြင်း

components/message-list.tsx ဖန်တီးပြီး အောက်ပါအတိုင်း ရေးပါ -

"use client";

import type { UIMessage } from "ai";

type MessageListProps = {
  messages: UIMessage[];
  status: "ready" | "submitted" | "streaming" | "error";
};

export function MessageList({ messages, status }: MessageListProps) {
  if (messages.length === 0) {
    return (
      <section className="message-list">
        <div className="empty-chat">
          <h2>Start a conversation</h2>
          <p>Ask a question or request a calculation.</p>
        </div>
      </section>
    );
  }

  return (
    <section className="message-list">
      {messages.map((message) => (
        <article key={message.id} className={`message message-${message.role}`}>
          <strong>{message.role === "user" ? "You" : "Assistant"}</strong>
          <div className="message-content">
            {message.parts.map((part, index) => {
              if (part.type === "text") {
                return <p key={`${message.id}-${index}`}>{part.text}</p>;
              }
              if (part.type === "tool-calculator") {
                return <ToolMessage key={`${message.id}-${index}`} part={part} />;
              }
              return null;
            })}
          </div>
        </article>
      ))}
      {status === "submitted" && (
        <div className="status-message">Assistant is thinking...</div>
      )}
    </section>
  );
}

function ToolMessage({ part }: { part: { state: string; input?: unknown; output?: unknown; } }) {
  if (part.state === "input-streaming" || part.state === "input-available") {
    return <div className="tool-message">Using calculator...</div>;
  }
  if (part.state === "output-available") {
    return (
      <details className="tool-message">
        <summary>Calculator result</summary>
        <pre>{JSON.stringify(part.output, null, 2)}</pre>
      </details>
    );
  }
  if (part.state === "output-error") {
    return <div className="tool-error">Calculator failed.</div>;
  }
  return null;
}

ဒီထိရပြီဆိုရင် Calculator tool အသုံးပြုနေတဲ့ အခြေအနေတွေကိုပါ ပြပေးနိုင်သွားပါပြီ။

၃၀။ Chat Input ခွဲထုတ်ခြင်း

components/chat-input.tsx ကို ဖန်တီးပါ -

"use client";

import type { KeyboardEvent, SubmitEvent } from "react";

type ChatInputProps = {
  input: string;
  isBusy: boolean;
  onInputChange: (value: string) => void;
  onSubmit: (event: SubmitEvent<HTMLFormElement>) => void;
  onStop: () => void;
};

export function ChatInput({
  input,
  isBusy,
  onInputChange,
  onSubmit,
  onStop,
}: ChatInputProps) {
  function handleKeyDown(event: KeyboardEvent<HTMLTextAreaElement>): void {
    if (event.key === "Enter" && !event.shiftKey) {
      event.preventDefault();
      event.currentTarget.form?.requestSubmit();
    }
  }

  return (
    <form className="chat-form" onSubmit={onSubmit}>
      <textarea
        value={input}
        onChange={(event) => onInputChange(event.target.value)}
        onKeyDown={handleKeyDown}
        placeholder="Type your message..."
        rows={3}
        disabled={isBusy}
      />
      {isBusy ? (
        <button type="button" className="stop-button" onClick={onStop}>
          Stop
        </button>
      ) : (
        <button type="submit" disabled={!input.trim()}>
          Send
        </button>
      )}
    </form>
  );
}

၃၁။ Main Chat Component ပြင်ဆင်ခြင်း

components/chat-app.tsx ကို အောက်ပါကုဒ်နဲ့ အစားထိုးလိုက်ပါ -

"use client";

import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport, type UIMessage } from "ai";
import { type SubmitEvent, useMemo, useState } from "react";
import { useRouter } from "next/navigation";

import { ChatInput } from "@/components/chat-input";
import { ChatSidebar } from "@/components/chat-sidebar";
import { MessageList } from "@/components/message-list";
import type { ChatSummary } from "@/lib/chat-store";

type ChatAppProps = {
  chatId: string;
  initialMessages?: UIMessage[];
  initialChats: ChatSummary[];
};

export function ChatApp({
  chatId,
  initialMessages = [],
  initialChats,
}: ChatAppProps) {
  const router = useRouter();
  const [input, setInput] = useState("");

  const transport = useMemo(
    () => new DefaultChatTransport({ api: "/api/chat" }),
    [],
  );

  const {
    messages,
    sendMessage,
    status,
    error,
    stop,
    clearError,
  } = useChat({
    id: chatId,
    messages: initialMessages,
    transport,
    onFinish: () => {
      router.refresh();
    },
    onError: (chatError) => {
      console.error("Client chat error:", chatError);
    },
  });

  const isBusy = status === "submitted" || status === "streaming";

  async function handleSubmit(event: SubmitEvent<HTMLFormElement>): Promise<void> {
    event.preventDefault();
    const message = input.trim();

    if (!message || isBusy) {
      return;
    }

    clearError();
    setInput("");

    try {
      await sendMessage({ text: message });
    } catch (sendError) {
      console.error("Could not send message:", sendError);
      setInput(message); // Restore input if failed
    }
  }

  return (
    <main className="application-layout">
      <ChatSidebar activeChatId={chatId} chats={initialChats} />

      <section className="chat-panel">
        <header className="chat-header">
          <div>
            <h1>My AI Agent</h1>
            <p>Streaming agent with persistent conversations</p>
          </div>
          <span className="chat-status">{getStatusLabel(status)}</span>
        </header>

        <MessageList messages={messages} status={status} />

        {error && (
          <div className="error-message" role="alert">
            <span>{error.message || "The assistant request failed."}</span>
            <button type="button" onClick={clearError}>Dismiss</button>
          </div>
        )}

        <ChatInput
          input={input}
          isBusy={isBusy}
          onInputChange={setInput}
          onSubmit={handleSubmit}
          onStop={stop}
        />
      </section>
    </main>
  );
}

function getStatusLabel(status: "ready" | "submitted" | "streaming" | "error"): string {
  switch (status) {
    case "ready": return "Ready";
    case "submitted": return "Thinking";
    case "streaming": return "Responding";
    case "error": return "Error";
  }
}

၃၂။ CSS Update

app/globals.css ကို အောက်ပါအတိုင်း အပြည့်အစုံ ပြောင်းလဲလိုက်ပါ -

* { box-sizing: border-box; }
html, body { margin: 0; min-height: 100%; }
body { overflow: hidden; background: #f4f5f7; color: #202124; font-family: Arial, Helvetica, sans-serif; }
a { color: inherit; text-decoration: none; }
button, textarea { font: inherit; }
button { cursor: pointer; }
button:disabled { cursor: not-allowed; opacity: 0.6; }

.application-layout { display: grid; grid-template-columns: 280px minmax(0, 1fr); height: 100vh; }
.chat-sidebar { display: flex; flex-direction: column; min-width: 0; padding: 16px; overflow: hidden; color: #f8fafc; background: #111827; }
.new-chat-button { display: block; margin-bottom: 16px; padding: 12px; text-align: center; background: #334155; border-radius: 8px; }
.new-chat-button:hover { background: #475569; }
.conversation-list { flex: 1; overflow-y: auto; }
.no-conversations { color: #94a3b8; font-size: 14px; }
.conversation-link { display: block; margin-bottom: 6px; padding: 11px 12px; border-radius: 8px; }
.conversation-link:hover { background: #1f2937; }
.conversation-link-active { background: #334155; }
.conversation-title { display: block; overflow: hidden; font-size: 14px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
.conversation-date { display: block; margin-top: 5px; color: #94a3b8; font-size: 12px; }

.chat-panel { display: flex; flex-direction: column; min-width: 0; height: 100vh; overflow: hidden; background: white; }
.chat-header { display: flex; align-items: center; justify-content: space-between; min-height: 76px; padding: 16px 24px; border-bottom: 1px solid #e5e7eb; }
.chat-header h1 { margin: 0; font-size: 20px; }
.chat-header p { margin: 5px 0 0; color: #6b7280; font-size: 13px; }
.chat-status { padding: 6px 10px; color: #475569; background: #f1f5f9; border-radius: 999px; font-size: 12px; }
.message-list { flex: 1; overflow-y: auto; padding: 28px; }
.empty-chat { max-width: 480px; margin: 120px auto; text-align: center; }
.empty-chat h2 { margin-bottom: 8px; }
.empty-chat p { color: #6b7280; }
.message { max-width: 760px; margin-bottom: 22px; padding: 14px 16px; border-radius: 12px; line-height: 1.6; }
.message strong { display: block; margin-bottom: 7px; font-size: 13px; }
.message-user { max-width: 70%; margin-left: auto; background: #dbeafe; }
.message-assistant { margin-right: auto; background: #f1f5f9; }
.message-content p { margin: 0 0 12px; white-space: pre-wrap; }
.message-content p:last-child { margin-bottom: 0; }
.status-message { color: #64748b; font-size: 14px; }
.tool-message { margin: 10px 0; padding: 10px; color: #475569; background: #e2e8f0; border-radius: 8px; font-size: 13px; }
.tool-message pre { overflow-x: auto; white-space: pre-wrap; }
.tool-error { margin-top: 10px; padding: 10px; color: #991b1b; background: #fee2e2; border-radius: 8px; }
.error-message { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 0 20px 12px; padding: 12px; color: #991b1b; background: #fee2e2; border-radius: 8px; }
.error-message button { padding: 5px 8px; color: #991b1b; background: transparent; border: 1px solid #fca5a5; border-radius: 6px; }
.chat-form { display: flex; gap: 12px; padding: 16px 20px 20px; border-top: 1px solid #e5e7eb; }
.chat-form textarea { flex: 1; resize: none; padding: 13px; border: 1px solid #cbd5e1; border-radius: 9px; outline: none; }
.chat-form textarea:focus { border-color: #64748b; }
.chat-form button { min-width: 90px; padding: 10px 18px; color: white; background: #1f2937; border: 0; border-radius: 8px; }
.chat-form .stop-button { background: #b91c1c; }

@media (max-width: 760px) {
  .application-layout { grid-template-columns: 1fr; }
  .chat-sidebar { display: none; }
  .message { max-width: 90%; }
  .message-list { padding: 18px; }
}

၃၃။ .gitignore

.gitignore ဖိုင်အောက်ဆုံးမှာ ဒါလေး ထပ်ထည့်ပေးပါ -

.data/

Chat တွေက JSON ဖိုင်တွေနဲ့ သိမ်းထားတာဖြစ်လို့ Git ထဲမှာ မပါသွားဖို့ အရေးကြီးပါတယ်။


၃၄။ App ကို ပြန် Run ခြင်း

npm run dev
  • Browser မှာ http://localhost:3000 ကို ဖွင့်လိုက်ရင် Chat အသစ်တစ်ခုဆီကို Redirect လုပ်သွားပါမယ်။
  • "My name is Lyra." လို့ ရိုက်ကြည့်ပါ။
  • ပြီးရင် "What is my name?" လို့ ထပ်မေးကြည့်ပါ။ ရှေ့က စကားကို မှတ်မိနေမှာပါ။
  • Page ကို Refresh လုပ်လိုက်ပါ။ Message တွေ မပျောက်သွားဘဲ ကျန်နေတာကို တွေ့ရပါမယ်။

၃၅။ Sidebar ကနေ + New chat နှိပ်ကြည့်ပါ

Chat အသစ်တစ်ခု ရောက်သွားတဲ့အခါ "What is my name?" လို့ ထပ်မေးကြည့်ပါ။ အခုတစ်ခါတော့ သူ သိမှာမဟုတ်တော့ပါဘူး။ ဒါက ဘာကိုပြသလဲဆိုရင် "Memory within one conversation" နဲ့ "Memory shared across every conversation" ဆိုတာ မတူညီဘူးဆိုတာပါပဲ။


ကျွန်တော်တို့ရဲ့ AI Agent လေးကို Web App တစ်ခုအဖြစ် အောင်မြင်စွာ ပြောင်းလဲတည်ဆောက်နိုင်ခဲ့ပြီ ဖြစ်ပါတယ်။

နောက်ထပ် လေ့လာဖို့အတွက်ဆိုရင်တော့ ဒီ JSON Storage ကနေ တကယ့် Database နည်းပညာတွေနဲ့ သိမ်းဆည်းတာ၊ User authentication ထည့်တာ စတာတွေကို ဆက်လုပ်ရမှာပဲ ဖြစ်ပါတယ်။

အားလုံးပဲ အဆင်ပြေကြပါစေ။

@hlahtunthein09

Copy link
Copy Markdown

ကျေးဇူးတင်ပါတယ်ဆရာ
ဒီ tutorial နှစ်ခုလုံးကို
openrouter က ပေးထားတဲ့
inclusionai/ling-3.0-flash:free

free model နဲ့ လိုက်လုပ်ခဲ့ပါတယ်ခင်ဗျ

ကျွန်တော့်ဆီမှာ
chat streaming က UI ပေါ်မှာ အလုပ်မလုပ်ဘူး ဖြစ်နေတယ်ခင်ဗျ

backend ဘက်က answer တွေ ကို chunk အလိုက် streaming လုပ်ပါတယ်

အဲ့တာ ကို powershell , git bash တို့ terminal မှာလဲ စမ်းကြည့်ပါတယ်
ပြီးတော့ browser ရဲ့ dev tool ထဲက network tab ထဲက
response , event stream tab တွေ ထဲမှာလဲ chat answer တွေ က chunk အလိုက် streaming လုပ်တာ တွေ့ ရပါတယ် ဒါပေမဲ့ UI ပေါ်မှာ တော့
Assistant is thinking လို့ ခနပြနေပြီး network tab ထဲက streaming ပြီးသွားမှ answer ကို တစ်ခါထဲ တန်းပြီး render လုပ်ပါတယ်ဆရာ
အဲ့လို streaming မလုပ်တာကို ပုံစံ မျိုးစုံ စမ်းကြည့်တာလဲ အဆင်မပြေလို့ပါဆရာ🙏

=====================================

Title: useChat with DefaultChatTransport does not stream UI message responses — full answer appears instantly

Description:

Following the tutorial, the backend streams text-delta chunks correctly (confirmed via browser Network tab and curl), but the frontend renders the entire assistant response all at once instead of word-by-word.

SDK versions: ai@7.0.48, @ai-sdk/react@4.0.51
Pattern used: streamText → toUIMessageStreamResponse on the backend, useChat + DefaultChatTransport on the frontend
Already tried: throttle, spreading message arrays, dynamic React keys, and the official Vercel docs example
Result: same behavior — server streams fine, UI batches updates and only shows the final message
It looks like a known issue in the AI SDK's useChat/DefaultChatTransport UI-message stream path, not a problem with the tutorial code itself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment