React-এ useChat Hook দিয়ে সেই Streaming Response আসলে UI-তে দেখানো

React-এ useChat Hook দিয়ে সেই Streaming Response আসলে UI-তে দেখানো

Z
জাহিদ হাসান তন্ময়
১৮ অক্টোবর, ২০২৬
13 মিনিটের পড়া
🇬🇧 Read in English
|
0 views
অডিও ওভারভিউ ও পডকাস্ট
~১ মিনিট কুইক ওভারভিউ
0%

React-এ useChat Hook দিয়ে সেই Streaming Response আসলে UI-তে দেখানো
React-এ useChat Hook দিয়ে সেই Streaming Response আসলে UI-তে দেখানো

আমাদের আগের পর্বে আমরা যখন Vercel AI SDK দিয়ে Next.js ব্যাকএন্ডে রিয়েল-টাইম Response Streaming তৈরি করেছিলাম, তখন streamText এবং /api/chat রাউট ব্যবহার করে প্রথম শব্দের অপেক্ষার সময় (TTFT) ৬ সেকেন্ড থেকে মাত্র ৩০০ মিলিসেকেন্ডে নামিয়ে এনেছিলাম। ব্যাকএন্ড থেকে তো স্ট্রিম আসা শুরু হলো, কিন্তু ফ্রন্টএন্ডে সাধারণ for await লুপ চালিয়ে স্ট্রিং জোড়া লাগানোর পদ্ধতিটি রিয়েল প্রোডাকশন চ্যাট ইউআই-তে কাজ করে না—বিশেষ করে যখন ইউজার মাঝপথে জেনারেশন থামাতে (Stop) চায়, নেটওয়ার্ক ফেইল করলে পুনরায় চেষ্টা (Retry) করতে চায়, কিংবা স্ট্রিমিং চলাকালীন স্ক্রল করে উপরের কোনো কোড পড়তে চায়।

React ফ্রন্টএন্ডে স্ট্রিমিং রেসপন্স নিখুঁতভাবে দেখানোর সবচেয়ে কার্যকর উপায় হলো Vercel AI SDK-এর useChat হুক ব্যবহার করে আমাদের /api/chat রাউটের সাথে যুক্ত করা। এই useChat হুকটি নিজে থেকেই অপটিমিস্টিক মেসেজ স্টেট, স্ট্রিম চাঙ্ক পার্সিং, AbortController দিয়ে মাঝপথে স্ট্রিম থামানো (stop()), এরর হলে রিলোড করা (reload()), এবং সার্ভার থেকে আসা টোকেন টেলিমেট্রি অ্যানোটেশন (annotations) হ্যান্ডেল করে—ফলে ম্যানুয়ালি শত শত লাইনের জটিল স্টেট রিডিউসার লেখার কোনো প্রয়োজন হয় না।

আজকের এই ১৬তম পর্বের মাধ্যমেই আমাদের DevPulse প্রজেক্টের প্রথম ধাপ (Phase 1: Core Brain & Streaming Pipeline, পর্ব ১৩–১৬) পূর্ণতা পাচ্ছে। আজ আমরা আমাদের এজেন্টের জন্য এমন একটি প্রোডাকশন-রেডি React UI তৈরি করব, যেখানে থাকবে লাইভ মডেল সুইচিং (Claude 3.5 Sonnet ও GPT-4o), স্মার্ট অটো-স্ক্রলিং, স্টপ বাটন এবং প্রতি মেসেজে রিয়েল-টাইম টোকেন কাউন্টার।

কেন ম্যানুয়াল for await লুপ প্রোডাকশন React UI-এর জন্য যথেষ্ট নয়?

১৫তম পর্বে আমরা স্ট্রিমিং টেস্ট করার জন্য setStreamedReply((prev) => prev + delta) ব্যবহার করে একটি ছোট টেস্ট কনসোল বানিয়েছিলাম। ব্যাকএন্ড চেক করার জন্য সেটি ঠিক থাকলেও একটি পূর্ণাঙ্গ চ্যাট অ্যাপ্লিকেশনে গেলে সেখানে ৫টি বড় সমস্যা সামনে আসে:

১. মাঝপথে স্ট্রিম থামানোর ব্যবস্থা না থাকা (AbortController): ধরুন ইউজার ভুল এরর লগ পেস্ট করে ফেলেছেন এবং প্রথম লাইন পড়েই বুঝতে পেরেছেন উত্তরটি তার কাজে লাগবে না। সাধারণ লুপে স্ট্রিম থামানোর কোনো বাটন না থাকায় মডেল পুরো ১০০০ টোকেন জেনারেট করে আপনার API ক্রেডিট নষ্ট করতেই থাকবে। ২. অতিরিক্ত Re-render ও UI ল্যাগ: দ্রুতগতির মডেলগুলো সেকেন্ডে ৭০-৮০টি টোকেন চাঙ্ক পাঠায়। প্রতি মিলিসেকেন্ডে সরাসরি র-স্টেট আপডেট করলে বড় মেসেজের ক্ষেত্রে ব্রাউজারের মেইন থ্রেড ধীর হয়ে যায়। ৩. মেসেজ আইডি ও ইনপুট স্টেট সিঙ্ক করা: ইউজারের প্রশ্ন, এজেন্টের স্ট্রিমিং উত্তর এবং ইনপুট বক্স ক্লিয়ার করার স্টেট আলাদাভাবে মেইনটেইন করতে গেলে কোড অযথা জটিল হয়ে পড়ে। ৪. নেটওয়ার্ক এরর রিকভারি (reload): স্ট্রিমিংয়ের মাঝপথে ইন্টারনেট কেটে গেলে ইউজারের শেষ প্রশ্নটি ডুপ্লিকেট না করে শুধুমাত্র এক ক্লিকে আবার সেই টার্নটি রান করার সুবিধা থাকা জরুরি। ৫. প্রতিটি মেসেজের সাথে সার্ভারের টোকেন হিসাব যুক্ত করা: স্ট্রিম শেষ হওয়ার পর ব্যাকএন্ডের onFinish থেকে পাওয়া promptTokens এবং latencyMs সরাসরি ওই নির্দিষ্ট মেসেজ বাবলের সাথে যুক্ত করে দেখানো।

useChat হুক এই পাঁচটি কাজই ভেতরে ভেতরে অত্যন্ত দক্ষতার সাথে সামলে নেয়।

রিয়েক্ট ইউজচ্যাট হুকের মাধ্যমে সায়ান টোকেন ফ্রেম, বেগুনি টেলিমেট্রি অ্যানোটেশন এবং অ্যাম্বার স্টপ কন্ট্রোল কাজ করার থ্রিডি ভিজ্যুয়ালাইজেশন
রিয়েক্ট ইউজচ্যাট হুকের মাধ্যমে সায়ান টোকেন ফ্রেম, বেগুনি টেলিমেট্রি অ্যানোটেশন এবং অ্যাম্বার স্টপ কন্ট্রোল কাজ করার থ্রিডি ভিজ্যুয়ালাইজেশন

useChat হুক কীভাবে /api/chat ব্যাকএন্ডের সাথে কাজ করে?

আপনি যখন কোনো Client Component-এর ভেতরে useChat({ api: '/api/chat' }) কল করেন, তখন এটি ব্রাউজারের React স্টেট এবং আমাদের Next.js Route Handler-এর মধ্যে একটি রিয়েল-টাইম ডেটা পাইপলাইন তৈরি করে।

নিচের সিকোয়েন্স ডায়াগ্রামে দেখুন ইউজারের সাবমিট করা থেকে শুরু করে স্টপ বাটন এবং টোকেন অ্যানোটেশন কীভাবে কাজ করে:

`mermaid sequenceDiagram participant User as ব্রাউজারে ইউজার participant Hook as React useChat Hook participant API as Next.js POST /api/chat participant LLM as OpenAI / Claude Stream

User->>Hook: প্রশ্ন সাবমিট + ডায়নামিক body (modelId)
Hook->>Hook: সাথে সাথে UI-তে ইউজার মেসেজ যোগ ও ইনপুট ক্লিয়ার
Hook->>API: POST messages[] + modelId (সাথে AbortSignal)
API->>LLM: streamText() শুরু
LLM-->>API: একটার পর একটা টোকেন ডেল্টা প্রদান
API-->>Hook: স্ট্রিম টেক্সট ফ্রেম প্রেরণ (0:"chunk")
Hook-->>User: স্টেট আপডেট ও স্মার্ট অটো-স্ক্রল
opt ইউজার যদি Stop বাটনে ক্লিক করে
    User->>Hook: stop() ফাংশন কল
    Hook->>API: সাথে সাথে AbortController.abort() ট্রিগার
end
LLM-->>API: জেনারেশন সম্পন্ন (Usage মেট্রিক্স)
API-->>Hook: মেসেজ অ্যানোটেশন প্রেরণ (modelUsed, tokens, latencyMs)
Hook-->>User: মেসেজের উপরে টোকেন ব্যাজ প্রদর্শন ও স্ট্রিম সমাপ্ত

`

আর্কিটেকচার ফ্লো সামারি (Architecture Flow Summary): ইউজার মেসেজ পাঠানোর সাথে সাথে useChat হুক অপটিমিস্টিকভাবে সেটি স্ক্রিনে দেখায় এবং একটি AbortSignal ও নির্বাচিত modelId-সহ /api/chat রাউটে POST রিকোয়েস্ট পাঠায়। সার্ভার থেকে আসা প্রতিটি টেক্সট ফ্রেম রিয়েল-টাইমে UI-তে রেন্ডার হয়, ইউজার চাইলে stop() কল করে যেকোনো মুহূর্তে কানেকশন কেটে দিতে পারে এবং উত্তর শেষ হলে সার্ভার থেকে পাঠানো টোকেন ও সময়ের হিসাব মেসেজের অ্যানোটেশন (annotations) হিসেবে যুক্ত হয়ে যায়।

ধাপ ১: /api/chat/route.ts আপগ্রেড করে টোকেন টেলিমেট্রি স্ট্রিম করা

গত পর্বে (পর্ব ১৫) আমাদের src/app/api/chat/route.ts ফাইলটি শুধু result.toDataStreamResponse() রিটার্ন করছিল। তাতে লেখাগুলো স্ট্রিম হলেও আমাদের onFinish কলব্যাকের ভেতরে পাওয়া টোকেন সংখ্যা ও ল্যাটেন্সির হিসাব ফ্রন্টএন্ডের useChat হুকে পৌঁছানোর কোনো পথ ছিল না।

এখন আমরা ai প্যাকেজের createDataStreamResponse ব্যবহার করে আমাদের Route Handler-টি আপডেট করব, যাতে টেক্সট স্ট্রিম শেষ হওয়ার সাথে সাথেই dataStream.writeMessageAnnotation()-এর মাধ্যমে টোকেন মেট্রিক্স সরাসরি ওই মেসেজের সাথে ব্রাউজারে চলে যায়:

`typescript // src/app/api/chat/route.ts import { NextRequest, NextResponse } from 'next/server'; import { createDataStreamResponse } from 'ai'; import { z } from 'zod'; import { defaultDevPulseConfig } from '@/lib/agent/blueprint'; import { type ChatMessage } from '@/lib/agent/llm-client'; import { createAgentTextStream } from '@/lib/agent/stream-client';

export const maxDuration = 60;

const routePayloadSchema = z.object({ sessionId: z.string().default('devpulse-default-session'), modelId: z .enum(['gpt-4o', 'claude-3-5-sonnet-latest']) .default('claude-3-5-sonnet-latest'), messages: z .array( z.object({ role: z.enum(['user', 'assistant', 'system']), content: z.string().min(1), }) ) .min(1), });

export interface MessageTelemetryAnnotation { modelUsed: string; promptTokens: number; completionTokens: number; latencyMs: number; }

export async function POST(req: NextRequest) { try { const body = await req.json(); const parsed = routePayloadSchema.safeParse(body);

if (!parsed.success) {
  return NextResponse.json(
    { error: 'Invalid chat request payload.' },
    { status: 400 }
  );
}

const { messages, modelId } = parsed.data;
const normalizedMessages: ChatMessage[] = messages
  .filter((m): m is { role: 'user' | 'assistant'; content: string } =>
    m.role === 'user' || m.role === 'assistant'
  );

return createDataStreamResponse({
  execute: (dataStream) => {
    const result = createAgentTextStream(
      normalizedMessages,
      {
        ...defaultDevPulseConfig,
        modelId,
      },
      (telemetry) => {
        dataStream.writeMessageAnnotation({
          modelUsed: telemetry.modelUsed,
          promptTokens: telemetry.promptTokens,
          completionTokens: telemetry.completionTokens,
          latencyMs: telemetry.latencyMs,
        });
      }
    );

    result.mergeIntoDataStream(dataStream);
  },
  onError: (error) => {
    return error instanceof Error
      ? error.message
      : 'Stream failed on server.';
  },
});

} catch (err) { const message = err instanceof Error ? err.message : 'Unexpected server error'; return NextResponse.json({ error: message }, { status: 500 }); } } `

খেয়াল করুন, dataStream.writeMessageAnnotation(...) কল করার কারণে আলাদা কোনো API কল বা অতিরিক্ত Server Action ছাড়াই একই স্ট্রিমিং কানেকশনের ভেতর দিয়ে আমরা টেক্সট এবং টেলিমেট্রি ডেটা দুটোই ফ্রন্টএন্ডে পাঠিয়ে দিচ্ছি।

ধাপ ২: স্মার্ট অটো-স্ক্রল হুক তৈরি করা (useSmartAutoScroll.ts)

চ্যাট কম্পোনেন্ট লেখার আগে স্ট্রিমিং ইউআই-এর একটি খুব কমন সমস্যা সমাধান করা দরকার, যাকে বলা হয় Scroll Hijacking।

এজেন্ট যখন বড় একটি উত্তর স্ট্রিম করতে থাকে, তখন নিচের নতুন লাইনগুলো দেখানোর জন্য চ্যাট বক্সটিকে অটো-স্ক্রল করে নিচে নামাতে হয়। কিন্তু আপনি যদি অন্ধভাবে প্রতি টোকেনে স্ক্রল নিচে নামাতে থাকেন, তবে কোনো ইউজার যদি স্ট্রিমিং চলাকালীন একটু উপরে স্ক্রল করে আগের কোনো কোড বা লাইন পড়তে চান, কোড তাকে জোর করে বারবার নিচে টেনে নামিয়ে আনবে! এটি ইউজারের জন্য প্রচণ্ড বিরক্তিকর।

তাই চলুন src/hooks/useSmartAutoScroll.ts নামে একটি কাস্টম হুক তৈরি করি, যা চেক করবে ইউজার চ্যাট বক্সের নিচের অংশেই (৬০ পিক্সেলের মধ্যে) আছেন কি না। ইউজার নিজে উপরে স্ক্রল করলে এটি অটো-স্ক্রল পজ করে দেবে:

`typescript // src/hooks/useSmartAutoScroll.ts 'use client';

import { useEffect, useRef, useState } from 'react';

export function useSmartAutoScroll(dependency: T) { const containerRef = useRef<HTMLDivElement | null>(null); const [shouldAutoScroll, setShouldAutoScroll] = useState(true);

function handleScroll() { const el = containerRef.current; if (!el) return;

const distanceFromBottom =
  el.scrollHeight - el.scrollTop - el.clientHeight;
setShouldAutoScroll(distanceFromBottom < 60);

}

useEffect(() => { const el = containerRef.current; if (!el || !shouldAutoScroll) return;

el.scrollTop = el.scrollHeight;

}, [dependency, shouldAutoScroll]);

return { containerRef, handleScroll, shouldAutoScroll, scrollToBottom: () => { const el = containerRef.current; if (el) { el.scrollTop = el.scrollHeight; setShouldAutoScroll(true); } }, }; } `

ধাপ ৩: useChat হুক দিয়ে সম্পূর্ণ চ্যাট UI তৈরি (DevPulseStreamChat.tsx)

এবার চলুন src/components/DevPulseStreamChat.tsx ফাইলটি তৈরি করি। এখানে আমরা ai/react থেকে useChat হুক ইমপোর্ট করে আমাদের মডেল সুইচার, স্টপ বাটন, এরর রিলোড এবং টেলিমেট্রি ব্যাজ যুক্ত করব:

`tsx // src/components/DevPulseStreamChat.tsx 'use client';

import { useState } from 'react'; import { useChat } from 'ai/react'; import { useSmartAutoScroll } from '@/hooks/useSmartAutoScroll'; import { type MessageTelemetryAnnotation } from '@/app/api/chat/route';

export default function DevPulseStreamChat() { const [modelId, setModelId] = useState<'claude-3-5-sonnet-latest' | 'gpt-4o'>( 'claude-3-5-sonnet-latest' );

const { messages, input, handleInputChange, handleSubmit, isLoading, stop, reload, error, } = useChat({ api: '/api/chat', id: 'devpulse-stream-session-01', });

const { containerRef, handleScroll, shouldAutoScroll, scrollToBottom } = useSmartAutoScroll(messages);

function onFormSubmit(e: React.FormEvent) { e.preventDefault(); if (!input.trim()) return;

handleSubmit(e, {
  body: {
    sessionId: 'devpulse-stream-session-01',
    modelId,
  },
});

}

return ( DevPulse Live Streaming Agent Powered by Vercel AI SDK useChat | TTFT ~300ms

    <select
      value={modelId}
      onChange={(e) =>
        setModelId(e.target.value as 'claude-3-5-sonnet-latest' | 'gpt-4o')
      }
      disabled={isLoading}
      className='bg-slate-950 border border-slate-700 rounded-lg px-3 py-1.5 text-xs text-slate-200 disabled:opacity-50'
    >
      <option value='claude-3-5-sonnet-latest'>Claude 3.5 Sonnet</option>
      <option value='gpt-4o'>OpenAI GPT-4o</option>
    </select>
  </div>

  <div
    ref={containerRef}
    onScroll={handleScroll}
    className='relative h-96 overflow-y-auto space-y-4 p-4 bg-slate-950 rounded-lg border border-slate-800/80'
  >
    {messages.length === 0 && (
      <div className='text-xs text-slate-500 text-center py-16 space-y-2'>
        <p>Ask DevPulse about API gateway rate limits, auth tokens, or open bugs.</p>
        <p className='text-slate-600'>
          Tokens stream in real time with per-turn telemetry annotations.
        </p>
      </div>
    )}

    {messages.map((m) => {
      const telemetry = (m.annotations?.[0] as unknown) as
        | MessageTelemetryAnnotation
        | undefined;

      return (
        <div
          key={m.id}
          className={`p-3.5 rounded-lg text-sm space-y-2 ${
            m.role === 'user'
              ? 'bg-cyan-950/40 border border-cyan-800/50 ml-12'
              : 'bg-slate-900 border border-slate-800 mr-12'
          }`}
        >
          <div className='flex items-center justify-between text-[10px] uppercase tracking-wider text-slate-400'>
            <span>{m.role === 'user' ? 'Developer' : 'DevPulse Agent'}</span>
            {telemetry && (
              <span className='text-cyan-400 font-mono lowercase'>
                {telemetry.modelUsed} • {telemetry.promptTokens} in /{' '}
                {telemetry.completionTokens} out • {telemetry.latencyMs}ms
              </span>
            )}
          </div>

          <div className='whitespace-pre-wrap leading-relaxed'>
            {m.content}
          </div>
        </div>
      );
    })}

    {!shouldAutoScroll && isLoading && (
      <button
        type='button'
        onClick={scrollToBottom}
        className='sticky bottom-2 left-1/2 -translate-x-1/2 px-3 py-1 rounded-full bg-cyan-700 text-xs font-medium shadow-lg'
      >
        Resume Auto-Scroll ↓
      </button>
    )}
  </div>

  {error && (
    <div className='p-3 rounded-lg bg-red-950/50 border border-red-800 text-xs text-red-200 flex items-center justify-between'>
      <span>Stream error: {error.message}</span>
      <button
        type='button'
        onClick={() => reload({ body: { modelId } })}
        className='px-2.5 py-1 rounded bg-red-800 hover:bg-red-700 text-white font-medium'
      >
        Retry Turn
      </button>
    </div>
  )}

  <form onSubmit={onFormSubmit} className='flex gap-2'>
    <input
      value={input}
      onChange={handleInputChange}
      placeholder='Ask a technical question or paste an error log...'
      className='flex-1 px-3.5 py-2 rounded-lg bg-slate-950 border border-slate-700 text-sm focus:outline-none focus:border-cyan-500'
    />

    {isLoading ? (
      <button
        type='button'
        onClick={stop}
        className='px-4 py-2 rounded-lg bg-amber-600 hover:bg-amber-500 text-sm font-semibold text-white'
      >
        Stop
      </button>
    ) : (
      <button
        type='submit'
        disabled={!input.trim()}
        className='px-4 py-2 rounded-lg bg-cyan-600 hover:bg-cyan-500 disabled:opacity-50 text-sm font-semibold text-white'
      >
        Send
      </button>
    )}
  </form>
</div>

); } `

ডার্ক ফ্রস্টেড গ্লাস চ্যাট ইন্টারফেসে স্মার্ট অটো-স্ক্রল প্রক্সিমিটি ট্র্যাকিং এবং মেসেজ কার্ডের ভিজ্যুয়াল রূপ
ডার্ক ফ্রস্টেড গ্লাস চ্যাট ইন্টারফেসে স্মার্ট অটো-স্ক্রল প্রক্সিমিটি ট্র্যাকিং এবং মেসেজ কার্ডের ভিজ্যুয়াল রূপ

আমার নিজের প্রজেক্টে স্ট্রিমিং স্ক্রলিং এবং Stale State নিয়ে একটি বাস্তব অভিজ্ঞতা

আমি যখন আমার Movie Explorer এবং GitHub Issues Tracker অ্যাপগুলোতে React স্টেট ও মডাল স্ক্রলিং নিয়ে কাজ করেছিলাম, তখন ইউজার ক্লিক করলেই কেবল একবার স্টেট আপডেট হতো। সেই অভ্যাসে আমি যখন প্রথমবার useChat দিয়ে স্ট্রিমিং চ্যাটবট ইউআই বানাই, তখন চ্যাট বক্স নিচে নামানোর জন্য useEffect-এর ভেতরে messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) লিখে দিয়েছিলাম।

ছোট এক-দুই লাইনের উত্তরের সময় কোনো সমস্যা বোঝা যায়নি। কিন্তু যেই মডেল সেকেন্ডে ৭০-৮০ টোকেন গতিতে একটা বড় কোড ব্লক স্ট্রিম করা শুরু করল, অমনি ক্রোম ব্রাউজারে পুরো চ্যাট উইন্ডোটি থরথর করে কাঁপতে (Layout Jitter) শুরু করল! কারণ প্রতি সেকেন্ডে ৫০ বার নতুন টোকেন আসার কারণে ব্রাউজার আগের smooth স্ক্রল অ্যানিমেশন শেষ করার আগেই আরেকটি অ্যানিমেশন কিউতে জমা করছিল। তার চেয়েও বড় বিপদ হলো—স্ট্রিমিং চলাকালীন আমি যখন মাউস ঘুরিয়ে উপরের কোডটুকু পড়তে চেষ্টা করলাম, প্রতি টোকেন আপডেটে কোড আমাকে জোর করে টেনে একদম নিচে নামিয়ে আনছিল!

একই সময়ে আমি আরেকটি সূক্ষ্ম বাগ ধরেছিলাম: ড্রপডাউন থেকে মডেল পাল্টে gpt-4o করার পরও প্রথম মেসেজে ব্যাকএন্ডে আগের claude-3-5-sonnet-latest চলে যাচ্ছিল, কারণ আমি modelId শুধুমাত্র শুরুতে useChat({ body })-এর ভেতরে দিয়ে রেখেছিলাম। পরবর্তীতে scrollIntoView({ behavior: 'smooth' }) বাদ দিয়ে সরাসরি el.scrollTop = el.scrollHeight এবং ৬০ পিক্সেলের থ্রেশহোল্ড চেক (useSmartAutoScroll) বসানোর পর স্ক্রলিং মাখনের মতো মসৃণ হয়ে যায়, আর handleSubmit(e, { body: { modelId } })-এর ভেতরে সরাসরি লেটেস্ট স্টেট পাস করায় মডেল সুইচিং বাগটিও চিরতরে দূর হয়ে যায়।

React-এ useChat ব্যবহারের সময় যে ৪টি ভুল এড়িয়ে চলবেন

পরের পর্বে (পর্ব ১৭) এজেন্টের ভেতরে লাইভ Tool Calling যুক্ত করার আগে নিশ্চিত হয়ে নিন আপনার ফ্রন্টএন্ড কোডে যেন এই ৪টি ভুল না থাকে:

১. প্রতি টোকেন আপডেটে behavior: 'smooth' ব্যবহার করা

কখনোই স্ট্রিমিং মেসেজের useEffect-এ scrollIntoView({ behavior: 'smooth' }) লিখবেন না। স্ট্রিমিংয়ের উচ্চ গতির সাথে স্মুথ স্ক্রল অ্যানিমেশন সংঘর্ষ তৈরি করে এবং পুরো স্ক্রিন কাঁপাতে থাকে। সবসময় আমাদের useSmartAutoScroll হুকের মতো সরাসরি scrollTop আপডেট করবেন।

২. মেসেজ লুপে key={m.id}-এর বদলে Array Index ব্যবহার করা

useChat প্রতিটি মেসেজের জন্য একটি ইউনিক m.id তৈরি করে। আপনি যদি অভ্যাসবশত messages.map((m, idx) => <div key={idx}>) লেখেন, তবে যখনই কোনো ফেইল হওয়া মেসেজ reload() বা ডিলিট করা হবে, তখন React একটি মেসেজের টেলিমেট্রি ব্যাজ অন্য মেসেজের সাথে গুলিয়ে ফেলবে।

৩. ইউআই-তে stop() বাটন না রাখা

অনেক ডেভেলপার শুধু Send বাটনটি isLoading অবস্থায় ডিসেবল করে রাখেন, কিন্তু কোনো Stop বাটন দেন না। এতে ইউজার ভুল প্রম্পট দিয়ে ফেললেও পুরো উত্তর শেষ না হওয়া পর্যন্ত অপেক্ষা করতে বাধ্য হন এবং আপনার অযথা আউটপুট টোকেন খরচ হয়। সবসময় isLoading অবস্থায় stop ফাংশন যুক্ত একটি বাটন দেখাবেন।

৪. ডায়নামিক স্টেট সরাসরি handleSubmit-এ পাস না করা

আপনার চ্যাট ইন্টারফেসে যদি মডেল সুইচার, টেম্পারেচার স্লাইডার বা প্রজেক্ট ফিল্টারের মতো ডায়নামিক React স্টেট থাকে, তবে সেটি শুধু useChat ইনিশিয়ালাইজেশনের সময় না দিয়ে সরাসরি handleSubmit(e, { body: { modelId } }) এবং reload({ body: { modelId } })-এর ভেতরে পাস করবেন—এতে কখনোই পুরনো (Stale) স্টেট সার্ভারে যাবে না।

প্রায়শই জিজ্ঞাসিত প্রশ্নাবলী (FAQ)

useChat হুক ব্যবহার করলে কি OpenAI এবং Claude—দুই মডেলের স্ট্রিমই একই ফ্রন্টএন্ড কোডে কাজ করবে?

হ্যাঁ, শতভাগ কাজ করবে। কারণ আমাদের /api/chat রাউটটি সার্ভার সাইডে streamText এবং createDataStreamResponse ব্যবহার করে OpenAI ও Anthropic উভয় মডেলের আউটপুটকেই একই স্ট্যান্ডার্ড ডেটা স্ট্রিম ফরম্যাটে রূপান্তর করে ব্রাউজারে পাঠায়। ফলে ফ্রন্টএন্ডের useChat কোডে প্রোভাইডারভেদে কোনো পরিবর্তনই করতে হয় না।

ইউজার যখন Stop বাটনে ক্লিক করে, তখন কি সার্ভারেও টোকেন জেনারেশন সাথে সাথে বন্ধ হয়ে যায়?

হ্যাঁ। ইউজার stop() কল করার সাথে সাথে ব্রাউজারের ভেতরে থাকা AbortController ওই চলমান HTTP রিকোয়েস্টটি বাতিল (abort) করে দেয়। Next.js সার্ভার সেই সংকেত পেয়ে সাথে সাথে OpenAI বা Claude-এর সাথে থাকা আপস্ট্রিম কানেকশন বন্ধ করে দেয়, ফলে পরের টোকেনগুলোর জন্য আপনার আর কোনো API খরচ হয় না।

সাধারণ টেক্সট ছাড়াও এজেন্ট যখন ভেতরে ভেতরে কোনো Tool কল করবে, useChat কি সেটাও UI-তে দেখাতে পারবে?

অবশ্যই পারবে! useChat-এর প্রতিটি মেসেজ অবজেক্টে content এবং annotations ছাড়াও toolInvocations নামের একটি বিশেষ অ্যারে থাকে, যা দিয়ে এজেন্ট কখন কোন টুল কল করছে এবং কী রেজাল্ট পাচ্ছে তা লাইভ দেখানো যায়—আর ঠিক এই জিনিসটিই আমরা আগামী পর্ব (পর্ব ১৭) থেকে আমাদের DevPulse এজেন্টে যুক্ত করতে যাচ্ছি।

🟢 ফ্রিল্যান্স ও কনট্রাক্ট প্রজেক্টের জন্য উন্মুক্ত

কাস্টম ওয়েব অ্যাপ বা এআই অটোমেশন বানাতে চান?

আমি স্টার্টআপ ও আধুনিক ব্যবসার জন্য স্কেলেবল ফুল-স্ট্যাক ওয়েব অ্যাপ্লিকেশন (Next.js, React, Node, PostgreSQL) এবং ইন্টেলিজেন্ট এআই এজেন্ট সলিউশন তৈরি করি। আপনার প্রজেক্টের আইডিয়া নিয়ে কথা বলা যাক!

ফুল-স্ট্যাক এমভিপি (MVP) ডেভেলপমেন্ট
অটোনোমাস এআই এজেন্ট ও এলএলএম ইন্টিগ্রেশন
পারফেক্ট এসইও ও সর্বোচ্চ পারফরম্যান্স
ক্লিন কোড ও মডার্ন আর্কিটেকচার

আর্টিকেলটি কি আপনার ভালো লেগেছে?

তন্ময়ের কাজকে সাপোর্ট করতে তালি (Clap) দিয়ে উৎসাহিত করুন!

0 views
Z

জাহিদ হাসান তন্ময়

সফটওয়্যার ডেভেলপার

MERN ফুল-স্ট্যাক ডেভেলপার এবং AI এজেন্ট ডেভেলপার, ঢাকা, বাংলাদেশ। ওয়েব ডেভেলপমেন্ট, রিঅ্যাক্ট, লারাভেল এবং আমার লার্নিং জার্নি নিয়ে লিখছি।

Related Articles

Stay Updated

Subscribe to get insights on full-stack architecture, AI agent engineering, and web development.