{"slug": "ai-sdk-asun-p-ur-web-ui-ttnny-cheaak-kh-ng-tutorial", "title": "AI SDK အသုံးပြု၍ Web UI တည်ဆောက်ခြင်း - Tutorial", "summary": "A developer built a web-based AI agent using TypeScript, Vercel AI SDK, OpenRouter, and Next.js, converting a terminal-only agent into a full web app with streaming responses, a React chat interface, conversation history, and multiple chat sessions. The architecture shifts from a terminal-to-agent flow to a browser-to-React-to-Next.js API route-to-agent pipeline, with conversations stored as JSON files. The tutorial covers setting up the project, installing necessary packages, and configuring environment variables.", "body_md": "[ပြီးခဲ့တဲ့ သင်ခန်းစာမှာ](https://gist.github.com/eimg/3856a76ec56848a0054518b09a1b804f) TypeScript, Vercel AI SDK နဲ့ OpenRouter တို့ကို အသုံးပြုပြီး AI agent တစ်ခုကို တည်ဆောက်ခဲ့ကြပါတယ်။\n\nကျွန်တော်တို့ရဲ့ Agent က -\n\n- Language model ခေါ်ယူခြင်း\n- Tool များအသုံးပြုခြင်း\n- Agent Loop လုပ်ဆောင်ခြင်း\n- Memory ယာယီသိမ်းဆည်းခြင်း\n- Subagent များအသုံးပြုခြင်း\n- Sequential Tasks များလုပ်ဆောင်ခြင်း\n- Parallel Tasks များလုပ်ဆောင်ခြင်း\n\nတို့ကို လုပ်ဆောင်နိုင်ခဲ့ပါတယ်။\n\nဒါပေမဲ့ အဲ့ဒီ Agent ဟာ Terminal ထဲမှာပဲ အလုပ်လုပ်တာ ဖြစ်ပါတယ်။\n\nဒီ သင်ခန်းစာမှာတော့ အဲ့ဒီ Agent ကို Web App အသေးလေးတစ်ခုအဖြစ် ပြောင်းလဲတည်ဆောက်သွားမှာ ဖြစ်ပါတယ်။\n\nသင်ခန်းစာ ပြီးဆုံးသွားတဲ့အခါ ကျွန်တော်တို့ရဲ့ App ဟာ -\n\n- Streaming responses\n- React Chat interface\n- Conversation history\n- Multiple chat sessions\n- Old conversations\n- Creating new conversations\n- Basic state management\n- Better error handling\n- Stopping a streaming response\n\nစတဲ့ အခြေခံလုပ်ဆောင်ချက် ပြည့်ပြည့်စုံစုံကို ရရှိသွားမှာ ဖြစ်ပါတယ်။\n\nအသုံးပြုသွားမယ့် နည်းပညာတွေကတော့ အရင်အတိုင်းပဲ -\n\n- TypeScript\n- npm\n- Vercel AI SDK\n- OpenRouter\n- Zod\n\nWeb APP အတွက်တော့ Next.js ကို အသုံးပြုသွားမှာပါ။\n\nDatabase တွေဘာတွေတော့ မသုံးသေးဘဲ Conversation တွေကို JSON ဖိုင်လေးတွေအနေနဲ့ပဲ သိမ်းဆည်းသွားပါမယ်။\n\nအရင်တုန်းက Architecture က ဒီလိုပါ -\n\n```\nUser\n  ↓\nTerminal\n  ↓\nAgent\n  ↓\nOpenRouter\n  ↓\nLanguage model\n```\n\nအသစ်ပြောင်းလဲမယ့် Architecture ကတော့ ဒီလို ဖြစ်သွားပါမယ် -\n\n```\nBrowser\n  ↓\nReact chat interface\n  ↓\nNext.js API route\n  ↓\nAI agent\n  ↓\nOpenRouter\n  ↓\nLanguage model\n```\n\nFile storage လည်း ထည့်သွင်းပါမယ် -\n\n```\nBrowser\n  ↓\nChat API\n  ↓\nAgent\n  ↓\nJSON conversation files\n```\n\nLanguage model က JSON ဖိုင်တွေကို တိုက်ရိုက်ဖတ်တာ ရေးတာ မလုပ်ပါဘူး။ ကျွန်တော်တို့ရဲ့ TypeScript ကုဒ်ကပဲ သိမ်းဆည်းတဲ့အပိုင်းကို လုပ်သွားမှာပါ။\n\nNext.js application အသစ်တစ်ခုကို အခုလို တည်ဆောက်ပါ -\n\n```\nnpx create-next-app@latest my-agent-web\n```\n\nSetup လုပ်တဲ့အခါ မေးခွန်းတွေ မေးလာရင် အောက်ပါအတိုင်း ရွေးချယ်ပေးပါ -\n\n```\nWould you like to use the recommended Next.js defaults? \n\nYes, use recommended defaults\n```\n\nပြီးရင် Project directory ထဲကို ဝင်ပါ -\n\n```\ncd my-agent-web\n```\n\nလိုအပ်တဲ့ AI packages တွေကို Install လုပ်ပါ -\n\n```\nnpm install ai @ai-sdk/react @openrouter/ai-sdk-provider zod\n```\n\nဒီ Packages တွေရဲ့ အဓိကလုပ်ဆောင်ချက်တွေကတော့ -\n\n`ai`\n\n: Vercel AI SDK ရဲ့ ပင်မ Package ဖြစ်ပါတယ်။`@ai-sdk/react`\n\n:`useChat()`\n\nလိုမျိုး React hooks တွေ ပါဝင်ပါတယ်။`@openrouter/ai-sdk-provider`\n\n: AI SDK နဲ့ OpenRouter ကို ချိတ်ဆက်ပေးပါတယ်။`zod`\n\n: Tool input တွေကို စစ်ဆေးဖို့ အသုံးပြုပါတယ်။\n\nProject ရဲ့ အပြင်ဘက်ဆုံး (Root) မှာ `.env.local`\n\nဖိုင်တစ်ခု ဖန်တီးပြီး အောက်ပါအတိုင်း ရေးထည့်ပါ -\n\n```\nOPENROUTER_API_KEY=your-openrouter-api-key\nOPENROUTER_MODEL=xiaomi/mimo-v2.5\n```\n\nOpenRouter Model ကို မိမိနှစ်သက်ရာ အခြား Model နဲ့လည်း အစားထိုးနိုင်ပါတယ်။ နောက်ပိုင်း Calculator tool ကို အသုံးပြုမှာဖြစ်လို့ ရွေးချယ်လိုက်တဲ့ Model က Tool calling ရဖို့တော့ လိုပါတယ်။\n\nAPI Key ကို Client-side React ကုဒ်တွေထဲမှာ မထည့်မိဖို့ကို သတိပြုပါ။ Server-side ဘက်မှာပဲ ရှိရပါမယ်။\n\nကျွန်တော်တို့ တဖြည်းဖြည်းချင်း တည်ဆောက်သွားမယ့် Folder Structure က အောက်ပါအတိုင်း ဖြစ်ပါတယ် -\n\n```\nmy-agent-web/\n├── app/\n│   ├── api/\n│   │   ├── chat/\n│   │   │   └── route.ts\n│   │   └── chats/\n│   │       └── route.ts\n│   ├── chat/\n│   │   ├── [id]/\n│   │   │   └── page.tsx\n│   │   └── page.tsx\n│   ├── globals.css\n│   ├── layout.tsx\n│   └── page.tsx\n├── components/\n│   ├── chat-app.tsx\n│   ├── chat-input.tsx\n│   ├── chat-sidebar.tsx\n│   └── message-list.tsx\n├── lib/\n│   ├── agent.ts\n│   ├── chat-store.ts\n│   ├── message-utils.ts\n│   └── tools.ts\n├── scripts/\n│   └── stream-test.ts\n├── .data/\n│   └── chats/\n├── .env.local\n├── package.json\n└── tsconfig.json\n```\n\n`.data/chats`\n\nfolder ဟာ ပထမဆုံး Chat History ကို သိမ်းဆည်းတဲ့အခါ အလိုအလျောက် ဖန်တီးသွားမှာပါ။\n\nပြီးခဲ့တဲ့ သင်ခန်းစာမှာ `generateText()`\n\nကို အသုံးပြုခဲ့ပါတယ် -\n\n``` js\nconst result = await generateText({\n  model,\n  prompt: \"Explain AI agents.\",\n});\n\nconsole.log(result.text);\n```\n\n`generateText()`\n\nဟာ စာတွေ အကုန်လုံး ထုတ်ပေးလို့ ပြီးတဲ့အထိ စောင့်ပါတယ်။\n\nFlow က ဒီလိုပါ -\n\n```\nSend prompt\n  ↓\nWait for complete generation\n  ↓\nReceive complete answer\n  ↓\nDisplay answer\n```\n\nStreaming ရဲ့ အလုပ်လုပ်ပုံကတော့ ကွဲပြားပါတယ် -\n\n```\nSend prompt\n  ↓\nReceive first piece\n  ↓\nDisplay it\n  ↓\nReceive next piece\n  ↓\nDisplay it\n  ↓\nContinue until complete\n```\n\nStreaming ဟာ Model ကို ပိုမြန်မြန် စာထုတ်ပေးအောင် လုပ်တာမဟုတ်ပါဘူး။ User အနေနဲ့ အဖြေအပြည့်အစုံ မပြီးခင်မှာကတည်းက စာတွေကို တဖြည်းဖြည်းချင်း မြင်တွေ့နိုင်အောင် လုပ်ပေးတာ ဖြစ်ပါတယ်။\n\nWeb interface မတည်ဆောက်ခင် Streaming သီးသန့် အလုပ်လုပ်ပုံကို အရင်စမ်းကြည့်ရအောင်။\n\n`scripts/stream-test.ts`\n\nဆိုပြီး ဖိုင်တစ်ခု ဖန်တီးပါ (scripts folder မရှိသေးရင် အသစ်ဆောက်ပါ)။ ပြီးတဲ့အခါ ဒီကုဒ်ကို ထည့်ပါ -\n\n``` js\nimport { createOpenRouter } from \"@openrouter/ai-sdk-provider\";\nimport { streamText } from \"ai\";\n\nasync function main() {\n  const apiKey = process.env.OPENROUTER_API_KEY;\n  const modelName = process.env.OPENROUTER_MODEL;\n\n  if (!apiKey) {\n    throw new Error(\"OPENROUTER_API_KEY is missing.\");\n  }\n\n  if (!modelName) {\n    throw new Error(\"OPENROUTER_MODEL is missing.\");\n  }\n\n  const openrouter = createOpenRouter({\n    apiKey,\n  });\n\n  const result = streamText({\n    model: openrouter(modelName),\n    prompt: \"Explain streaming responses in simple terms.\",\n  });\n\n  for await (const textPart of result.textStream) {\n    process.stdout.write(textPart);\n  }\n\n  process.stdout.write(\"\\n\");\n}\n\nmain().catch((error) => {\n  console.error(error);\n  process.exitCode = 1;\n});\n```\n\nဒီဖိုင်ကို သီးသန့် Run လို့ရအောင် `tsx`\n\nကို install လုပ်ပါ -\n\n```\nnpm install -D tsx\n```\n\n`package.json`\n\nထဲက `scripts`\n\nမှာ `stream-test`\n\nကို သွားထည့်ပေးပါ (ရှိပြီးသား Next.js script တွေကို မဖျက်ပါနဲ့) -\n\n```\n{\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\",\n    \"stream-test\": \"tsx --env-file=.env.local scripts/stream-test.ts\"\n  }\n}\n```\n\nအခုလို Run ကြည့်ပါ -\n\n```\nnpm run stream-test\n```\n\nစာတွေ တဖြည်းဖြည်းချင်း Stream အနေနဲ့ ထွက်လာတာကို တွေ့ရပါလိမ့်မယ်။\n\nအဓိကကုဒ်ကတော့ `streamText`\n\nကို အသုံးပြုထားတာပါပဲ -\n\n``` js\nconst result = streamText({\n  model,\n  prompt,\n});\n```\n\nပြီးတော့ ရလာတဲ့ အပိုင်းအစလေးတွေကို အခုလို loop ပတ်ပြီး ထုတ်ယူပါတယ် -\n\n``` js\nfor await (const textPart of result.textStream) {\n  process.stdout.write(textPart);\n}\n```\n\nAgent နဲ့သက်ဆိုင်တဲ့ Logic တွေကို API route ထဲမှာ အကုန်မရေးဘဲ သီးသန့်ခွဲထုတ်ပါမယ်။\n\n`lib/agent.ts`\n\nကို ဖန်တီးပြီး အောက်ပါအတိုင်း ရေးပါ -\n\n``` js\nimport { createOpenRouter } from \"@openrouter/ai-sdk-provider\";\n\nconst apiKey = process.env.OPENROUTER_API_KEY;\nconst modelName = process.env.OPENROUTER_MODEL;\n\nif (!apiKey) {\n  throw new Error(\"OPENROUTER_API_KEY is missing.\");\n}\n\nif (!modelName) {\n  throw new Error(\"OPENROUTER_MODEL is missing.\");\n}\n\nconst openrouter = createOpenRouter({\n  apiKey,\n});\n\nexport const agentModel = openrouter(modelName);\n\nexport const agentSystemPrompt = `\nYou are a helpful AI assistant.\n\nExplain ideas clearly and directly.\nUse the calculator tool whenever accurate arithmetic is required.\nAfter using a tool, explain the result to the user.\n\nDo not claim that you performed an action unless a tool actually performed it.\n`;\n```\n\nဒီ module ထဲမှာ -\n\n- Provider သတ်မှတ်ခြင်း\n- Model ရွေးချယ်ခြင်း\n- System instructions များ ပါဝင်ပါတယ်။\n\nUI တွေ၊ HTTP request တွေ၊ သိမ်းဆည်းတဲ့အပိုင်းတွေကို ဒီထဲမှာ မထည့်ပါဘူး။ သီးခြားခွဲထားမှာပါ။\n\n`lib/tools.ts`\n\nကို ဖန်တီးပြီး အောက်ပါအတိုင်း ရေးပါ -\n\n``` js\nimport { tool } from \"ai\";\nimport { z } from \"zod\";\n\nexport const calculatorTool = tool({\n  description:\n    \"Perform accurate arithmetic using two numbers. Use this tool when arithmetic is required.\",\n\n  inputSchema: z.object({\n    operation: z\n      .enum([\"add\", \"subtract\", \"multiply\", \"divide\"])\n      .describe(\"The arithmetic operation to perform\"),\n\n    a: z.number().describe(\"The first number\"),\n\n    b: z.number().describe(\"The second number\"),\n  }),\n\n  execute: async ({ operation, a, b }) => {\n    switch (operation) {\n      case \"add\":\n        return {\n          success: true,\n          result: a + b,\n        };\n\n      case \"subtract\":\n        return {\n          success: true,\n          result: a - b,\n        };\n\n      case \"multiply\":\n        return {\n          success: true,\n          result: a * b,\n        };\n\n      case \"divide\":\n        if (b === 0) {\n          return {\n            success: false,\n            error: \"Division by zero is not allowed.\",\n          };\n        }\n\n        return {\n          success: true,\n          result: a / b,\n        };\n    }\n  },\n});\n\nexport const agentTools = {\n  calculator: calculatorTool,\n};\n```\n\nTools တွေကို Object တစ်ခုတည်းအနေနဲ့ ပေါင်းပြီး Export လုပ်ထားပါတယ် -\n\n``` js\nexport const agentTools = {\n  calculator: calculatorTool,\n};\n```\n\nဒီလိုလုပ်ထားတာက `streamText()`\n\nမှာပဲဖြစ်ဖြစ်၊ Message တွေ Validate လုပ်တဲ့အခါမှာပဲဖြစ်ဖြစ် အလွယ်တကူ ထည့်သွင်းအသုံးပြုနိုင်ဖို့ ဖြစ်ပါတယ်။\n\n`app/api/chat/route.ts`\n\nကို ဖန်တီးပြီး အောက်ပါအတိုင်း ရေးပါ -\n\n``` js\nimport {\n  convertToModelMessages,\n  createIdGenerator,\n  type InferUITools,\n  stepCountIs,\n  streamText,\n  type UIDataTypes,\n  type UIMessage,\n  validateUIMessages,\n} from \"ai\";\n\nimport {\n  agentModel,\n  agentSystemPrompt,\n} from \"@/lib/agent\";\n\nimport { agentTools } from \"@/lib/tools\";\n\nexport const runtime = \"nodejs\";\n\ntype AgentUIMessage = UIMessage<\n  never,\n  UIDataTypes,\n  InferUITools<typeof agentTools>\n>;\n\ntype ChatRequestBody = {\n  id?: string;\n  messages?: AgentUIMessage[];\n};\n\nexport async function POST(request: Request): Promise<Response> {\n  try {\n    const body = (await request.json()) as ChatRequestBody;\n\n    if (!body.id) {\n      return Response.json(\n        {\n          error: \"Chat ID is required.\",\n        },\n        {\n          status: 400,\n        },\n      );\n    }\n\n    if (!Array.isArray(body.messages)) {\n      return Response.json(\n        {\n          error: \"Messages must be an array.\",\n        },\n        {\n          status: 400,\n        },\n      );\n    }\n\n    const validatedMessages = await validateUIMessages<AgentUIMessage>({\n      messages: body.messages,\n      tools: agentTools,\n    });\n\n    const modelMessages =\n      await convertToModelMessages(validatedMessages);\n\n    const result = streamText({\n      model: agentModel,\n\n      system: agentSystemPrompt,\n\n      messages: modelMessages,\n\n      tools: agentTools,\n\n      stopWhen: stepCountIs(5),\n    });\n\n    return result.toUIMessageStreamResponse({\n      originalMessages: validatedMessages,\n\n      generateMessageId: createIdGenerator({\n        prefix: \"msg\",\n        size: 16,\n      }),\n\n      onError: (error) => {\n        console.error(\"Streaming error:\", error);\n\n        return \"The assistant could not complete the response.\";\n      },\n    });\n  } catch (error) {\n    console.error(\"Chat request failed:\", error);\n\n    return Response.json(\n      {\n        error: \"The chat request failed.\",\n      },\n      {\n        status: 500,\n      },\n    );\n  }\n}\n```\n\nအခုလောလောဆယ်တော့ History သိမ်းဆည်းတဲ့ အပိုင်းကို မထည့်ရသေးပါဘူး။\n\nဒီ API endpoint က -\n\n- Message တွေကို လက်ခံတယ်\n- မှန်မမှန် စစ်ဆေးတယ်\n- UI message ကနေ Model message ကို ပြောင်းပေးတယ်\n- Model ကို လှမ်းခေါ်တယ်\n- Tool တွေ လိုအပ်ရင် run ပေးတယ်\n- Browser ဆီကို Response ကို Streaming ပုံစံနဲ့ ပြန်ပို့ပေးပါတယ်။\n\nBrowser နဲ့ Model ဟာ Message format အမျိုးအစား မတူညီတာကို သုံးကြပါတယ်။\n\nReact UI က `UIMessage[]`\n\nကို သုံးပါတယ် -\n\n```\ntype UIMessage = {\n  id: string;\n  role: \"user\" | \"assistant\" | \"system\";\n  parts: UIMessagePart[];\n};\n```\n\nModel ကတော့ `ModelMessage[]`\n\nကို သုံးပါတယ်။ ဒါကြောင့် `convertToModelMessages`\n\nနဲ့ ပြောင်းလဲပေးရတာပါ -\n\n``` js\nconst modelMessages = await convertToModelMessages(validatedMessages);\n```\n\nFlow က ဒီလိုပါ -\n\n```\nBrowser UIMessage[]\n  ↓\nconvertToModelMessages()\n  ↓\nModelMessage[]\n  ↓\nLanguage model\n```\n\nနောက်ပိုင်း သိမ်းဆည်းတဲ့အခါမှာ UI ဖက်က လိုအပ်တဲ့ ဖွဲ့စည်းပုံတွေ ပါဝင်တဲ့ `UIMessage`\n\nတွေကိုပဲ အဓိကထား သိမ်းဆည်းသွားမှာပါ။\n\nBrowser ကနေ Server ဆီ Data တွေ ပို့တဲ့အခါ မှန်ကန်တယ်လို့ အမြဲမယူဆသင့်ပါဘူး။ ဒီကုဒ်က ဝင်လာတဲ့ Message တွေကို စစ်ဆေးပေးပါတယ် -\n\n``` js\nconst validatedMessages = await validateUIMessages<AgentUIMessage>({\n  messages: body.messages,\n  tools: agentTools,\n});\n```\n\nTool calls တွေ၊ Tool results တွေ၊ Metadata တွေ ပါလာတဲ့အခါ အမှားအယွင်းမရှိအောင် Validate လုပ်ပေးခြင်းက Tool information အမှားတွေ Model ဆီ ရောက်မသွားအောင် ကာကွယ်ပေးပါတယ်။\n\n`streamText()`\n\nကနေ ပြန်ရလာတဲ့ Result ဟာ ရိုးရိုး JSON object မဟုတ်ပါဘူး။ Active stream တစ်ခု ဖြစ်ပါတယ်။\nဒါကို `useChat()`\n\nက နားလည်တဲ့ HTTP response အဖြစ် ပြောင်းလဲပေးဖို့ ဒီ method ကို သုံးရတာပါ -\n\n```\nreturn result.toUIMessageStreamResponse();\n```\n\n`components/chat-app.tsx`\n\nကို ဖန်တီးပြီး အောက်ပါအတိုင်း ရေးပါ -\n\n``` js\n\"use client\";\n\nimport { useChat } from \"@ai-sdk/react\";\nimport {\n  DefaultChatTransport,\n  type UIMessage,\n} from \"ai\";\n\nimport {\n  type SubmitEvent,\n  useState,\n} from \"react\";\n\ntype ChatAppProps = {\n  chatId: string;\n  initialMessages?: UIMessage[];\n};\n\nexport function ChatApp({\n  chatId,\n  initialMessages = [],\n}: ChatAppProps) {\n  const [input, setInput] = useState(\"\");\n\n  const {\n    messages,\n    sendMessage,\n    status,\n    error,\n    stop,\n  } = useChat({\n    id: chatId,\n\n    messages: initialMessages,\n\n    transport: new DefaultChatTransport({\n      api: \"/api/chat\",\n    }),\n\n    onError: (chatError) => {\n      console.error(\"Chat error:\", chatError);\n    },\n  });\n\n  const isBusy =\n    status === \"submitted\" ||\n    status === \"streaming\";\n\n  async function handleSubmit(\n    event: SubmitEvent<HTMLFormElement>,\n  ): Promise<void> {\n    event.preventDefault();\n\n    const message = input.trim();\n\n    if (!message || isBusy) {\n      return;\n    }\n\n    setInput(\"\");\n\n    await sendMessage({\n      text: message,\n    });\n  }\n\n  return (\n    <main className=\"chat-page\">\n      <section className=\"chat-panel\">\n        <header className=\"chat-header\">\n          <div>\n            <h1>My AI Agent</h1>\n            <p>Chat ID: {chatId}</p>\n          </div>\n\n          {isBusy && (\n            <button\n              type=\"button\"\n              className=\"secondary-button\"\n              onClick={stop}\n            >\n              Stop\n            </button>\n          )}\n        </header>\n\n        <section className=\"message-list\">\n          {messages.length === 0 && (\n            <div className=\"empty-chat\">\n              <h2>Start a conversation</h2>\n              <p>\n                Ask a question or request a calculation.\n              </p>\n            </div>\n          )}\n\n          {messages.map((message) => (\n            <article\n              key={message.id}\n              className={`message message-${message.role}`}\n            >\n              <strong>\n                {message.role === \"user\"\n                  ? \"You\"\n                  : \"Assistant\"}\n              </strong>\n\n              <div className=\"message-content\">\n                {message.parts.map((part, index) => {\n                  if (part.type === \"text\") {\n                    return (\n                      <p key={`${message.id}-${index}`}>\n                        {part.text}\n                      </p>\n                    );\n                  }\n\n                  return null;\n                })}\n              </div>\n            </article>\n          ))}\n\n          {status === \"submitted\" && (\n            <div className=\"status-message\">\n              Assistant is thinking...\n            </div>\n          )}\n        </section>\n\n        {error && (\n          <div className=\"error-message\">\n            {error.message ||\n              \"Something went wrong while generating the response.\"}\n          </div>\n        )}\n\n        <form\n          className=\"chat-form\"\n          onSubmit={handleSubmit}\n        >\n          <textarea\n            value={input}\n            onChange={(event) =>\n              setInput(event.target.value)\n            }\n            placeholder=\"Type your message...\"\n            rows={3}\n            disabled={isBusy}\n          />\n\n          <button\n            type=\"submit\"\n            disabled={!input.trim() || isBusy}\n          >\n            Send\n          </button>\n        </form>\n      </section>\n    </main>\n  );\n}\n```\n\nအဓိက အစိတ်အပိုင်းကတော့ ဒါပါ -\n\n```\nconst {\n  messages,\n  sendMessage,\n  status,\n  error,\n  stop,\n} = useChat({ ... });\n```\n\n`useChat()`\n\nက Client-side ရဲ့ Chat state ကို စီမံပေးပါတယ်။\n\n`messages`\n\n: လက်ရှိ ပြသပေးနေတဲ့ messages တွေ`sendMessage`\n\n: User ရဲ့ Message ကို ပို့ပေးမယ့် Function`status`\n\n: လက်ရှိ Request ရဲ့ အခြေအနေ`error`\n\n: Error တက်ရင် ပြပေးမယ့် အချက်အလက်`stop`\n\n: အလုပ်လုပ်နေတဲ့ Stream ကို ရပ်ပစ်မယ့် Function\n\nStatus တွေက အောက်ပါအတိုင်း ဖြစ်နိုင်ပါတယ် -\n\n`ready`\n\n: နောက်ထပ် Message လက်ခံဖို့ အသင့်ဖြစ်နေတဲ့ အချိန်`submitted`\n\n: Request ပို့ပြီးပြီ၊ ဒါပေမဲ့ Response ပြန်မလာသေးတဲ့ အချိန်`streaming`\n\n: Response တဖြည်းဖြည်းချင်း ဝင်လာနေတဲ့ အချိန်`error`\n\n: Request ပို့တာ Fail ဖြစ်သွားတဲ့ အချိန်\n\nUI Message တစ်ခုမှာ စာသားတွေကို တိုက်ရိုက်သိမ်းထားတာ မဟုတ်ဘဲ `parts`\n\nအနေနဲ့ သိမ်းထားတာပါ -\n\n``` js\n{message.parts.map((part, index) => {\n  if (part.type === \"text\") {\n    return (\n      <p key={`${message.id}-${index}`}>\n        {part.text}\n      </p>\n    );\n  }\n  return null;\n})}\n```\n\nနောက်ပိုင်းမှာ Tool calls တွေ၊ Results တွေလည်း ဒီ `parts`\n\nထဲမှာ ပါလာမှာ ဖြစ်ပေမဲ့ အခုလောလောဆယ်တော့ Text ကိုပဲ ပြပေးထားပါတယ်။\n\n`app/page.tsx`\n\nကို ဖွင့်ပြီး အောက်ပါအတိုင်း အစားထိုးပါ -\n\n``` js\nimport { ChatApp } from \"@/components/chat-app\";\n\nexport default function HomePage() {\n  return (\n    <ChatApp\n      chatId=\"temporary-chat\"\n      initialMessages={[]}\n    />\n  );\n}\n```\n\nအခုချိန်မှာ Persistence တို့၊ Multiple chats တို့ မပါသေးပါဘူး။ Streaming ကို စမ်းဖို့အတွက်ပဲ ဖြစ်ပါတယ်။\n\n`app/globals.css`\n\nထဲက စာတွေကို အောက်ပါကုဒ်နဲ့ အစားထိုးလိုက်ပါ -\n\n```\n* { box-sizing: border-box; }\nhtml, body { margin: 0; min-height: 100%; }\nbody { background: #f4f5f7; color: #202124; font-family: Arial, Helvetica, sans-serif; }\nbutton, textarea { font: inherit; }\nbutton { cursor: pointer; }\nbutton:disabled { cursor: not-allowed; opacity: 0.6; }\n.chat-page { min-height: 100vh; padding: 24px; }\n.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; }\n.chat-header { display: flex; align-items: center; justify-content: space-between; padding: 18px 20px; border-bottom: 1px solid #e5e7eb; }\n.chat-header h1 { margin: 0; font-size: 20px; }\n.chat-header p { margin: 4px 0 0; color: #6b7280; font-size: 12px; }\n.message-list { flex: 1; overflow-y: auto; padding: 24px; }\n.empty-chat { max-width: 480px; margin: 100px auto; text-align: center; }\n.empty-chat h2 { margin-bottom: 8px; }\n.empty-chat p { color: #6b7280; }\n.message { max-width: 75%; margin-bottom: 18px; padding: 14px 16px; border-radius: 12px; line-height: 1.55; }\n.message strong { display: block; margin-bottom: 6px; font-size: 13px; }\n.message-user { margin-left: auto; background: #e8f0fe; }\n.message-assistant { margin-right: auto; background: #f1f3f4; }\n.message-content p { margin: 0; white-space: pre-wrap; }\n.status-message { color: #6b7280; font-size: 14px; }\n.error-message { margin: 0 20px 12px; padding: 12px; color: #991b1b; background: #fee2e2; border-radius: 8px; }\n.chat-form { display: flex; gap: 12px; padding: 16px; border-top: 1px solid #e5e7eb; }\n.chat-form textarea { flex: 1; resize: none; padding: 12px; border: 1px solid #cbd5e1; border-radius: 8px; }\n.chat-form button, .secondary-button { padding: 10px 18px; color: white; background: #1f2937; border: 0; border-radius: 8px; }\n.secondary-button { background: #64748b; }\nnpm run dev\n```\n\n`http://localhost:3000`\n\nကို ဖွင့်ပြီး စမ်းသပ်ကြည့်ပါ။\n\n\"Explain what an AI agent is.\" လို့ ရိုက်ထည့်ကြည့်ပါ၊ အဖြေတွေ တဖြည်းဖြည်း ထွက်လာတာ တွေ့ရပါမယ်။\n\nပြီးရင် \"Calculate 48 multiplied by 17.\" လို့ ရိုက်ကြည့်ပါ၊ Tool ကို လှမ်းသုံးသွားတာကိုပါ တွေ့ရမှာပါ။\n\nလက်ရှိ Flow က ဒီလို အလုပ်လုပ်နေတာပါ -\n\n```\nUser types message\n  ↓\nuseChat() sends UI messages\n  ↓\nPOST /api/chat\n  ↓\nMessages are validated and converted\n  ↓\nstreamText() calls the model\n  ↓\nTool may execute\n  ↓\nResponse streams to the browser\n  ↓\nuseChat() updates the message\n  ↓\nReact rerenders\n```\n\nအခုချိန်မှာ Browser ကပဲ Message တွေကို မှတ်ထားပေးတာဖြစ်လို့ Page ကို Refresh လုပ်လိုက်ရင် ပျောက်သွားမှာပါ။\n\nဒါကြောင့် Chat တွေကို JSON ဖိုင်တွေအနေနဲ့ `.data/chats/`\n\nအောက်မှာ သိမ်းဆည်းပေးမယ့်စနစ် လိုအပ်ပါတယ်။ Database သုံးမယ့်အစား အလွယ်တကူ လေ့လာနိုင်ဖို့ File storage ကိုပဲ သုံးပါမယ်။\n\n`lib/chat-store.ts`\n\nကို ဖန်တီးပြီး အောက်ပါအတိုင်း အပြည့်အစုံ ရေးထည့်ပါ -\n\n``` js\nimport {\n  createIdGenerator,\n  type UIMessage,\n} from \"ai\";\n\nimport {\n  mkdir,\n  readFile,\n  readdir,\n  rename,\n  writeFile,\n} from \"node:fs/promises\";\n\nimport path from \"node:path\";\n\nexport type StoredChat = {\n  id: string;\n  title: string;\n  messages: UIMessage[];\n  createdAt: string;\n  updatedAt: string;\n};\n\nexport type ChatSummary = {\n  id: string;\n  title: string;\n  createdAt: string;\n  updatedAt: string;\n};\n\nconst createChatId = createIdGenerator({\n  prefix: \"chat\",\n  size: 16,\n});\n\nconst chatDirectory = path.join(\n  process.cwd(),\n  \".data\",\n  \"chats\",\n);\n\nasync function ensureChatDirectory(): Promise<void> {\n  await mkdir(chatDirectory, {\n    recursive: true,\n  });\n}\n\nfunction getChatFilePath(chatId: string): string {\n  if (!/^[a-zA-Z0-9_-]+$/.test(chatId)) {\n    throw new Error(\"Invalid chat ID.\");\n  }\n\n  return path.join(\n    chatDirectory,\n    `${chatId}.json`,\n  );\n}\n\nfunction getTextFromMessage(\n  message: UIMessage,\n): string {\n  return message.parts\n    .filter(\n      (\n        part,\n      ): part is Extract<\n        UIMessage[\"parts\"][number],\n        { type: \"text\" }\n      > => part.type === \"text\",\n    )\n    .map((part) => part.text)\n    .join(\"\")\n    .trim();\n}\n\nfunction createChatTitle(\n  messages: UIMessage[],\n): string {\n  const firstUserMessage = messages.find(\n    (message) => message.role === \"user\",\n  );\n\n  if (!firstUserMessage) {\n    return \"New conversation\";\n  }\n\n  const text = getTextFromMessage(\n    firstUserMessage,\n  );\n\n  if (!text) {\n    return \"New conversation\";\n  }\n\n  const maximumLength = 48;\n\n  if (text.length <= maximumLength) {\n    return text;\n  }\n\n  return `${text.slice(0, maximumLength)}…`;\n}\n\nasync function writeJsonFile(\n  filePath: string,\n  value: unknown,\n): Promise<void> {\n  const temporaryPath = `${filePath}.tmp`;\n\n  const json = JSON.stringify(\n    value,\n    null,\n    2,\n  );\n\n  await writeFile(\n    temporaryPath,\n    json,\n    \"utf8\",\n  );\n\n  await rename(\n    temporaryPath,\n    filePath,\n  );\n}\n\nexport async function createChat(): Promise<StoredChat> {\n  await ensureChatDirectory();\n\n  const now = new Date().toISOString();\n\n  const chat: StoredChat = {\n    id: createChatId(),\n    title: \"New conversation\",\n    messages: [],\n    createdAt: now,\n    updatedAt: now,\n  };\n\n  await writeJsonFile(\n    getChatFilePath(chat.id),\n    chat,\n  );\n\n  return chat;\n}\n\nexport async function loadChat(\n  chatId: string,\n): Promise<StoredChat | null> {\n  await ensureChatDirectory();\n\n  try {\n    const content = await readFile(\n      getChatFilePath(chatId),\n      \"utf8\",\n    );\n\n    return JSON.parse(content) as StoredChat;\n  } catch (error) {\n    const nodeError =\n      error as NodeJS.ErrnoException;\n\n    if (nodeError.code === \"ENOENT\") {\n      return null;\n    }\n\n    throw error;\n  }\n}\n\nexport async function saveChat({\n  chatId,\n  messages,\n}: {\n  chatId: string;\n  messages: UIMessage[];\n}): Promise<StoredChat> {\n  const existingChat =\n    await loadChat(chatId);\n\n  if (!existingChat) {\n    throw new Error(\n      `Chat \"${chatId}\" does not exist.`,\n    );\n  }\n\n  const updatedChat: StoredChat = {\n    ...existingChat,\n    title: createChatTitle(messages),\n    messages,\n    updatedAt: new Date().toISOString(),\n  };\n\n  await writeJsonFile(\n    getChatFilePath(chatId),\n    updatedChat,\n  );\n\n  return updatedChat;\n}\n\nexport async function listChats(): Promise<\n  ChatSummary[]\n> {\n  await ensureChatDirectory();\n\n  const filenames = await readdir(\n    chatDirectory,\n  );\n\n  const chatFiles = filenames.filter(\n    (filename) =>\n      filename.endsWith(\".json\") &&\n      !filename.endsWith(\".tmp\"),\n  );\n\n  const chats = await Promise.all(\n    chatFiles.map(async (filename) => {\n      try {\n        const content = await readFile(\n          path.join(chatDirectory, filename),\n          \"utf8\",\n        );\n\n        const chat =\n          JSON.parse(content) as StoredChat;\n\n        return {\n          id: chat.id,\n          title: chat.title,\n          createdAt: chat.createdAt,\n          updatedAt: chat.updatedAt,\n        } satisfies ChatSummary;\n      } catch (error) {\n        console.error(\n          `Could not read ${filename}:`,\n          error,\n        );\n\n        return null;\n      }\n    }),\n  );\n\n  return chats\n    .filter(\n      (\n        chat,\n      ): chat is ChatSummary =>\n        chat !== null,\n    )\n    .sort(\n      (first, second) =>\n        new Date(second.updatedAt).getTime() -\n        new Date(first.updatedAt).getTime(),\n    );\n}\n```\n\nဒီဖိုင်ထဲမှာ Chat အသစ်တည်ဆောက်တာ၊ ဖတ်တာ၊ သိမ်းတာ၊ စာရင်းထုတ်တာတွေကို လုပ်ဆောင်ပေးမယ့် Function တွေ ပါဝင်ပါတယ်။ JSON ဖိုင်ကို Save တဲ့အခါ ယာယီ `.tmp`\n\nဖိုင်တစ်ခု အရင်ရေးပြီးမှ အမည်ပြောင်းတဲ့နည်း (Safe Write) ကို အသုံးပြုထားပါတယ်။\n\n`app/api/chat/route.ts`\n\nကို ဖွင့်ပြီး `loadChat`\n\n, `saveChat`\n\nကို import လုပ်ကာ `POST`\n\nfunction ထဲက `onFinish`\n\nမှာ သိမ်းဆည်းမယ့် လုပ်ဆောင်ချက် ထည့်ပါမယ်။ ဖိုင်အပြည့်အစုံကို အောက်ပါအတိုင်း အစားထိုးလိုက်ပါ -\n\n``` js\nimport {\n  convertToModelMessages,\n  createIdGenerator,\n  type InferUITools,\n  stepCountIs,\n  streamText,\n  type UIDataTypes,\n  type UIMessage,\n  validateUIMessages,\n} from \"ai\";\n\nimport {\n  agentModel,\n  agentSystemPrompt,\n} from \"@/lib/agent\";\n\nimport {\n  loadChat,\n  saveChat,\n} from \"@/lib/chat-store\";\n\nimport { agentTools } from \"@/lib/tools\";\n\nexport const runtime = \"nodejs\";\n\ntype AgentUIMessage = UIMessage<\n  never,\n  UIDataTypes,\n  InferUITools<typeof agentTools>\n>;\n\ntype ChatRequestBody = {\n  id?: string;\n  messages?: AgentUIMessage[];\n};\n\nexport async function POST(\n  request: Request,\n): Promise<Response> {\n  try {\n    const body =\n      (await request.json()) as ChatRequestBody;\n\n    if (!body.id) {\n      return Response.json(\n        { error: \"Chat ID is required.\" },\n        { status: 400 },\n      );\n    }\n\n    if (!Array.isArray(body.messages)) {\n      return Response.json(\n        { error: \"Messages must be an array.\" },\n        { status: 400 },\n      );\n    }\n\n    const storedChat = await loadChat(body.id);\n\n    if (!storedChat) {\n      return Response.json(\n        { error: \"Chat was not found.\" },\n        { status: 404 },\n      );\n    }\n\n    const validatedMessages = await validateUIMessages<AgentUIMessage>({\n      messages: body.messages,\n      tools: agentTools,\n    });\n\n    const modelMessages = await convertToModelMessages(validatedMessages);\n\n    const result = streamText({\n      model: agentModel,\n      system: agentSystemPrompt,\n      messages: modelMessages,\n      tools: agentTools,\n      stopWhen: stepCountIs(5),\n    });\n\n    return result.toUIMessageStreamResponse({\n      originalMessages: validatedMessages,\n\n      generateMessageId: createIdGenerator({\n        prefix: \"msg\",\n        size: 16,\n      }),\n\n      onFinish: async ({ messages }) => {\n        try {\n          await saveChat({\n            chatId: body.id!,\n            messages,\n          });\n        } catch (error) {\n          console.error(\"Could not save chat:\", error);\n        }\n      },\n\n      onError: (error) => {\n        console.error(\"Streaming error:\", error);\n        return \"The assistant could not complete the response.\";\n      },\n    });\n  } catch (error) {\n    console.error(\"Chat request failed:\", error);\n    return Response.json(\n      { error: \"The chat request failed.\" },\n      { status: 500 },\n    );\n  }\n}\n```\n\n`onFinish`\n\nထဲမှာ ပါလာတဲ့ `messages`\n\nတွေက အစအဆုံး အကုန်ပါဝင်တဲ့အတွက် အဲ့ဒါကို တိုက်ရိုက် Save လုပ်လိုက်ရုံပါပဲ။ Streaming ပြီးတာနဲ့ သိမ်းလိုက်တဲ့ သဘောပါ။\n\n`app/chat/page.tsx`\n\nဖိုင်ဖန်တီးပြီး အခုလို ရေးပါ -\n\n``` js\nimport { redirect } from \"next/navigation\";\nimport { createChat } from \"@/lib/chat-store\";\n\nexport const dynamic = \"force-dynamic\";\n\nexport default async function NewChatPage() {\n  const chat = await createChat();\n  redirect(`/chat/${chat.id}`);\n}\n```\n\n`/chat`\n\nကို ဝင်လိုက်တာနဲ့ Chat အသစ်တစ်ခု ဆောက်ပြီး ချက်ချင်း Redirect လုပ်သွားမှာပါ။\n\n`app/page.tsx`\n\nကို အခုလို ပြင်ပါ -\n\n``` js\nimport { redirect } from \"next/navigation\";\n\nexport default function HomePage() {\n  redirect(\"/chat\");\n}\n```\n\n`app/chat/[id]/page.tsx`\n\nကို ဖန်တီးပြီး အခုလို ရေးပါ -\n\n``` js\nimport { notFound } from \"next/navigation\";\nimport { ChatApp } from \"@/components/chat-app\";\nimport {\n  listChats,\n  loadChat,\n} from \"@/lib/chat-store\";\n\nexport const dynamic = \"force-dynamic\";\n\ntype ChatPageProps = {\n  params: Promise<{\n    id: string;\n  }>;\n};\n\nexport default async function ChatPage({\n  params,\n}: ChatPageProps) {\n  const { id } = await params;\n\n  const [chat, chats] = await Promise.all([\n    loadChat(id),\n    listChats(),\n  ]);\n\n  if (!chat) {\n    notFound();\n  }\n\n  return (\n    <ChatApp\n      chatId={chat.id}\n      initialMessages={chat.messages}\n      initialChats={chats}\n    />\n  );\n}\n```\n\nဒီနေရာမှာ `Promise.all`\n\nကိုသုံးပြီး လိုအပ်တဲ့ data နှစ်ခုကို Parallel ယူထားတာ သတိပြုပါ။\n\n`components/chat-app.tsx`\n\nမှာ `initialChats`\n\nထပ်ထည့်ပေးဖို့ လိုလာပါပြီ။ ခဏနေရင် Sidebar လည်း ထည့်ရမှာမို့ `ChatApp`\n\nတစ်ခုလုံးကို နောက်ပိုင်းကျရင် အပြည့်အစုံ အစားထိုးပါမယ်။\n\n`components/chat-sidebar.tsx`\n\nဖန်တီးပြီး အခုလို ရေးပါ -\n\n``` python\n\"use client\";\n\nimport Link from \"next/link\";\nimport type { ChatSummary } from \"@/lib/chat-store\";\n\ntype ChatSidebarProps = {\n  activeChatId: string;\n  chats: ChatSummary[];\n};\n\nexport function ChatSidebar({\n  activeChatId,\n  chats,\n}: ChatSidebarProps) {\n  return (\n    <aside className=\"chat-sidebar\">\n      <Link href=\"/chat\" className=\"new-chat-button\">\n        + New chat\n      </Link>\n\n      <nav className=\"conversation-list\">\n        {chats.length === 0 && (\n          <p className=\"no-conversations\">\n            No conversations yet.\n          </p>\n        )}\n\n        {chats.map((chat) => (\n          <Link\n            key={chat.id}\n            href={`/chat/${chat.id}`}\n            className={\n              chat.id === activeChatId\n                ? \"conversation-link conversation-link-active\"\n                : \"conversation-link\"\n            }\n          >\n            <span className=\"conversation-title\">{chat.title}</span>\n            <span className=\"conversation-date\">{formatDate(chat.updatedAt)}</span>\n          </Link>\n        ))}\n      </nav>\n    </aside>\n  );\n}\n\nfunction formatDate(dateString: string): string {\n  const date = new Date(dateString);\n  return new Intl.DateTimeFormat(undefined, {\n    month: \"short\",\n    day: \"numeric\",\n  }).format(date);\n}\n```\n\n`components/message-list.tsx`\n\nဖန်တီးပြီး အောက်ပါအတိုင်း ရေးပါ -\n\n``` python\n\"use client\";\n\nimport type { UIMessage } from \"ai\";\n\ntype MessageListProps = {\n  messages: UIMessage[];\n  status: \"ready\" | \"submitted\" | \"streaming\" | \"error\";\n};\n\nexport function MessageList({ messages, status }: MessageListProps) {\n  if (messages.length === 0) {\n    return (\n      <section className=\"message-list\">\n        <div className=\"empty-chat\">\n          <h2>Start a conversation</h2>\n          <p>Ask a question or request a calculation.</p>\n        </div>\n      </section>\n    );\n  }\n\n  return (\n    <section className=\"message-list\">\n      {messages.map((message) => (\n        <article key={message.id} className={`message message-${message.role}`}>\n          <strong>{message.role === \"user\" ? \"You\" : \"Assistant\"}</strong>\n          <div className=\"message-content\">\n            {message.parts.map((part, index) => {\n              if (part.type === \"text\") {\n                return <p key={`${message.id}-${index}`}>{part.text}</p>;\n              }\n              if (part.type === \"tool-calculator\") {\n                return <ToolMessage key={`${message.id}-${index}`} part={part} />;\n              }\n              return null;\n            })}\n          </div>\n        </article>\n      ))}\n      {status === \"submitted\" && (\n        <div className=\"status-message\">Assistant is thinking...</div>\n      )}\n    </section>\n  );\n}\n\nfunction ToolMessage({ part }: { part: { state: string; input?: unknown; output?: unknown; } }) {\n  if (part.state === \"input-streaming\" || part.state === \"input-available\") {\n    return <div className=\"tool-message\">Using calculator...</div>;\n  }\n  if (part.state === \"output-available\") {\n    return (\n      <details className=\"tool-message\">\n        <summary>Calculator result</summary>\n        <pre>{JSON.stringify(part.output, null, 2)}</pre>\n      </details>\n    );\n  }\n  if (part.state === \"output-error\") {\n    return <div className=\"tool-error\">Calculator failed.</div>;\n  }\n  return null;\n}\n```\n\nဒီထိရပြီဆိုရင် Calculator tool အသုံးပြုနေတဲ့ အခြေအနေတွေကိုပါ ပြပေးနိုင်သွားပါပြီ။\n\n`components/chat-input.tsx`\n\nကို ဖန်တီးပါ -\n\n``` python\n\"use client\";\n\nimport type { KeyboardEvent, SubmitEvent } from \"react\";\n\ntype ChatInputProps = {\n  input: string;\n  isBusy: boolean;\n  onInputChange: (value: string) => void;\n  onSubmit: (event: SubmitEvent<HTMLFormElement>) => void;\n  onStop: () => void;\n};\n\nexport function ChatInput({\n  input,\n  isBusy,\n  onInputChange,\n  onSubmit,\n  onStop,\n}: ChatInputProps) {\n  function handleKeyDown(event: KeyboardEvent<HTMLTextAreaElement>): void {\n    if (event.key === \"Enter\" && !event.shiftKey) {\n      event.preventDefault();\n      event.currentTarget.form?.requestSubmit();\n    }\n  }\n\n  return (\n    <form className=\"chat-form\" onSubmit={onSubmit}>\n      <textarea\n        value={input}\n        onChange={(event) => onInputChange(event.target.value)}\n        onKeyDown={handleKeyDown}\n        placeholder=\"Type your message...\"\n        rows={3}\n        disabled={isBusy}\n      />\n      {isBusy ? (\n        <button type=\"button\" className=\"stop-button\" onClick={onStop}>\n          Stop\n        </button>\n      ) : (\n        <button type=\"submit\" disabled={!input.trim()}>\n          Send\n        </button>\n      )}\n    </form>\n  );\n}\n```\n\n`components/chat-app.tsx`\n\nကို အောက်ပါကုဒ်နဲ့ အစားထိုးလိုက်ပါ -\n\n``` js\n\"use client\";\n\nimport { useChat } from \"@ai-sdk/react\";\nimport { DefaultChatTransport, type UIMessage } from \"ai\";\nimport { type SubmitEvent, useMemo, useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\n\nimport { ChatInput } from \"@/components/chat-input\";\nimport { ChatSidebar } from \"@/components/chat-sidebar\";\nimport { MessageList } from \"@/components/message-list\";\nimport type { ChatSummary } from \"@/lib/chat-store\";\n\ntype ChatAppProps = {\n  chatId: string;\n  initialMessages?: UIMessage[];\n  initialChats: ChatSummary[];\n};\n\nexport function ChatApp({\n  chatId,\n  initialMessages = [],\n  initialChats,\n}: ChatAppProps) {\n  const router = useRouter();\n  const [input, setInput] = useState(\"\");\n\n  const transport = useMemo(\n    () => new DefaultChatTransport({ api: \"/api/chat\" }),\n    [],\n  );\n\n  const {\n    messages,\n    sendMessage,\n    status,\n    error,\n    stop,\n    clearError,\n  } = useChat({\n    id: chatId,\n    messages: initialMessages,\n    transport,\n    onFinish: () => {\n      router.refresh();\n    },\n    onError: (chatError) => {\n      console.error(\"Client chat error:\", chatError);\n    },\n  });\n\n  const isBusy = status === \"submitted\" || status === \"streaming\";\n\n  async function handleSubmit(event: SubmitEvent<HTMLFormElement>): Promise<void> {\n    event.preventDefault();\n    const message = input.trim();\n\n    if (!message || isBusy) {\n      return;\n    }\n\n    clearError();\n    setInput(\"\");\n\n    try {\n      await sendMessage({ text: message });\n    } catch (sendError) {\n      console.error(\"Could not send message:\", sendError);\n      setInput(message); // Restore input if failed\n    }\n  }\n\n  return (\n    <main className=\"application-layout\">\n      <ChatSidebar activeChatId={chatId} chats={initialChats} />\n\n      <section className=\"chat-panel\">\n        <header className=\"chat-header\">\n          <div>\n            <h1>My AI Agent</h1>\n            <p>Streaming agent with persistent conversations</p>\n          </div>\n          <span className=\"chat-status\">{getStatusLabel(status)}</span>\n        </header>\n\n        <MessageList messages={messages} status={status} />\n\n        {error && (\n          <div className=\"error-message\" role=\"alert\">\n            <span>{error.message || \"The assistant request failed.\"}</span>\n            <button type=\"button\" onClick={clearError}>Dismiss</button>\n          </div>\n        )}\n\n        <ChatInput\n          input={input}\n          isBusy={isBusy}\n          onInputChange={setInput}\n          onSubmit={handleSubmit}\n          onStop={stop}\n        />\n      </section>\n    </main>\n  );\n}\n\nfunction getStatusLabel(status: \"ready\" | \"submitted\" | \"streaming\" | \"error\"): string {\n  switch (status) {\n    case \"ready\": return \"Ready\";\n    case \"submitted\": return \"Thinking\";\n    case \"streaming\": return \"Responding\";\n    case \"error\": return \"Error\";\n  }\n}\n```\n\n`app/globals.css`\n\nကို အောက်ပါအတိုင်း အပြည့်အစုံ ပြောင်းလဲလိုက်ပါ -\n\n```\n* { box-sizing: border-box; }\nhtml, body { margin: 0; min-height: 100%; }\nbody { overflow: hidden; background: #f4f5f7; color: #202124; font-family: Arial, Helvetica, sans-serif; }\na { color: inherit; text-decoration: none; }\nbutton, textarea { font: inherit; }\nbutton { cursor: pointer; }\nbutton:disabled { cursor: not-allowed; opacity: 0.6; }\n\n.application-layout { display: grid; grid-template-columns: 280px minmax(0, 1fr); height: 100vh; }\n.chat-sidebar { display: flex; flex-direction: column; min-width: 0; padding: 16px; overflow: hidden; color: #f8fafc; background: #111827; }\n.new-chat-button { display: block; margin-bottom: 16px; padding: 12px; text-align: center; background: #334155; border-radius: 8px; }\n.new-chat-button:hover { background: #475569; }\n.conversation-list { flex: 1; overflow-y: auto; }\n.no-conversations { color: #94a3b8; font-size: 14px; }\n.conversation-link { display: block; margin-bottom: 6px; padding: 11px 12px; border-radius: 8px; }\n.conversation-link:hover { background: #1f2937; }\n.conversation-link-active { background: #334155; }\n.conversation-title { display: block; overflow: hidden; font-size: 14px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }\n.conversation-date { display: block; margin-top: 5px; color: #94a3b8; font-size: 12px; }\n\n.chat-panel { display: flex; flex-direction: column; min-width: 0; height: 100vh; overflow: hidden; background: white; }\n.chat-header { display: flex; align-items: center; justify-content: space-between; min-height: 76px; padding: 16px 24px; border-bottom: 1px solid #e5e7eb; }\n.chat-header h1 { margin: 0; font-size: 20px; }\n.chat-header p { margin: 5px 0 0; color: #6b7280; font-size: 13px; }\n.chat-status { padding: 6px 10px; color: #475569; background: #f1f5f9; border-radius: 999px; font-size: 12px; }\n.message-list { flex: 1; overflow-y: auto; padding: 28px; }\n.empty-chat { max-width: 480px; margin: 120px auto; text-align: center; }\n.empty-chat h2 { margin-bottom: 8px; }\n.empty-chat p { color: #6b7280; }\n.message { max-width: 760px; margin-bottom: 22px; padding: 14px 16px; border-radius: 12px; line-height: 1.6; }\n.message strong { display: block; margin-bottom: 7px; font-size: 13px; }\n.message-user { max-width: 70%; margin-left: auto; background: #dbeafe; }\n.message-assistant { margin-right: auto; background: #f1f5f9; }\n.message-content p { margin: 0 0 12px; white-space: pre-wrap; }\n.message-content p:last-child { margin-bottom: 0; }\n.status-message { color: #64748b; font-size: 14px; }\n.tool-message { margin: 10px 0; padding: 10px; color: #475569; background: #e2e8f0; border-radius: 8px; font-size: 13px; }\n.tool-message pre { overflow-x: auto; white-space: pre-wrap; }\n.tool-error { margin-top: 10px; padding: 10px; color: #991b1b; background: #fee2e2; border-radius: 8px; }\n.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; }\n.error-message button { padding: 5px 8px; color: #991b1b; background: transparent; border: 1px solid #fca5a5; border-radius: 6px; }\n.chat-form { display: flex; gap: 12px; padding: 16px 20px 20px; border-top: 1px solid #e5e7eb; }\n.chat-form textarea { flex: 1; resize: none; padding: 13px; border: 1px solid #cbd5e1; border-radius: 9px; outline: none; }\n.chat-form textarea:focus { border-color: #64748b; }\n.chat-form button { min-width: 90px; padding: 10px 18px; color: white; background: #1f2937; border: 0; border-radius: 8px; }\n.chat-form .stop-button { background: #b91c1c; }\n\n@media (max-width: 760px) {\n  .application-layout { grid-template-columns: 1fr; }\n  .chat-sidebar { display: none; }\n  .message { max-width: 90%; }\n  .message-list { padding: 18px; }\n}\n```\n\n`.gitignore`\n\nဖိုင်အောက်ဆုံးမှာ ဒါလေး ထပ်ထည့်ပေးပါ -\n\n```\n.data/\n```\n\nChat တွေက JSON ဖိုင်တွေနဲ့ သိမ်းထားတာဖြစ်လို့ Git ထဲမှာ မပါသွားဖို့ အရေးကြီးပါတယ်။\n\n```\nnpm run dev\n```\n\n- Browser မှာ\n`http://localhost:3000`\n\nကို ဖွင့်လိုက်ရင် Chat အသစ်တစ်ခုဆီကို Redirect လုပ်သွားပါမယ်။ - \"My name is Lyra.\" လို့ ရိုက်ကြည့်ပါ။\n- ပြီးရင် \"What is my name?\" လို့ ထပ်မေးကြည့်ပါ။ ရှေ့က စကားကို မှတ်မိနေမှာပါ။\n- Page ကို Refresh လုပ်လိုက်ပါ။ Message တွေ မပျောက်သွားဘဲ ကျန်နေတာကို တွေ့ရပါမယ်။\n\nChat အသစ်တစ်ခု ရောက်သွားတဲ့အခါ \"What is my name?\" လို့ ထပ်မေးကြည့်ပါ။ အခုတစ်ခါတော့ သူ သိမှာမဟုတ်တော့ပါဘူး။ ဒါက ဘာကိုပြသလဲဆိုရင် \"Memory within one conversation\" နဲ့ \"Memory shared across every conversation\" ဆိုတာ မတူညီဘူးဆိုတာပါပဲ။\n\nကျွန်တော်တို့ရဲ့ AI Agent လေးကို Web App တစ်ခုအဖြစ် အောင်မြင်စွာ ပြောင်းလဲတည်ဆောက်နိုင်ခဲ့ပြီ ဖြစ်ပါတယ်။\n\nနောက်ထပ် လေ့လာဖို့အတွက်ဆိုရင်တော့ ဒီ JSON Storage ကနေ တကယ့် Database နည်းပညာတွေနဲ့ သိမ်းဆည်းတာ၊ User authentication ထည့်တာ စတာတွေကို ဆက်လုပ်ရမှာပဲ ဖြစ်ပါတယ်။\n\nအားလုံးပဲ အဆင်ပြေကြပါစေ။", "url": "https://wpnews.pro/news/ai-sdk-asun-p-ur-web-ui-ttnny-cheaak-kh-ng-tutorial", "canonical_source": "https://gist.github.com/eimg/f374804f3b68de1e3d3e82f6bae6e52e", "published_at": "2026-07-17 06:52:34+00:00", "updated_at": "2026-08-01 04:58:36.611113+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-agents", "developer-tools", "large-language-models"], "entities": ["TypeScript", "Vercel AI SDK", "OpenRouter", "Next.js", "Zod", "React", "npm"], "alternates": {"html": "https://wpnews.pro/news/ai-sdk-asun-p-ur-web-ui-ttnny-cheaak-kh-ng-tutorial", "markdown": "https://wpnews.pro/news/ai-sdk-asun-p-ur-web-ui-ttnny-cheaak-kh-ng-tutorial.md", "text": "https://wpnews.pro/news/ai-sdk-asun-p-ur-web-ui-ttnny-cheaak-kh-ng-tutorial.txt", "jsonld": "https://wpnews.pro/news/ai-sdk-asun-p-ur-web-ui-ttnny-cheaak-kh-ng-tutorial.jsonld"}}