Panduan Lanjutan
Di panduan ini, Anda akan membangun aplikasi chat siap produksi menggunakan adapter framework, lapisan server yang aman, dan pola streaming yang andal.
Ringkasan
Panduan ini melengkapi Quick Start dengan pola dunia nyata yang biasa dipakai di produksi:
- Adapter framework untuk UI bersih tanpa repot fetch manual.
- Lapisan server aman yang menjaga kunci tidak bocor ke klien.
- Konsumsi streaming di server (satukan pesan akhir asisten sebelum merespons).
Fitur Utama
- Pola UI berbasis adapter untuk React, Vue, SvelteKit, Angular, dan Nuxt.
- Dua strategi server:
- Stream‑through (klien merender token saat tiba).
- Server‑consume (server menggabungkan SSE menjadi jawaban akhir, lalu mengirim JSON).
Integrasi UI per framework
Gunakan adapter resmi untuk menghindari plumbing fetch/state manual. Setiap adapter menyediakan primitif chat tingkat tinggi (mis., hook useChat atau store/kelas Chat) yang mengelola state pesan, status streaming, tombol stop/regenerate, dan penanganan error. Di balik layar, klien ini melakukan POST ke proxy server Anda (disarankan /api/chat) dan merender teks saat mengalir. UI Anda cukup merender messages dan parts‑nya (teks, pemanggilan Tools). Tampilkan tombol Stop ketika status adalah submitted atau streaming. Utamakan adapter untuk framework Anda; hanya gunakan fetch + SSE mentah bila alur sangat khusus. Jika perlu menjaga rahasia di luar klien atau menggabungkan hasil, lihat bagian Server Side di bawah.
Catatan: Opsi DefaultChatTransport seperti headers dan body bisa diberikan sebagai objek statis (nilai tidak berubah) atau sebagai fungsi async yang mengembalikan objek. Gunakan bentuk fungsi async bila nilainya dinamis (mis. token sesi/pengguna, metadata per‑request); pakai nilai statis hanya jika benar‑benar konstan.
Next.js / React (@ai-sdk/react)
Pertama, instal dependensi yang diperlukan:
npm install ai @ai-sdk/react
# atau
yarn add ai @ai-sdk/react
# atau
pnpm add ai @ai-sdk/reactKemudian gunakan di komponen Anda:
// app/page.tsx
'use client';
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport, generateId } from 'ai';
export default function Chat() {
const networkTransport = new DefaultChatTransport({
api: 'https://api.korinai.com/api/chat',
headers: async () => {
// Ambil token sesi/pengguna dari lapisan auth Anda (jangan expose API key di klien)
// Misalnya, getAuthToken merupakan sebuah fungsi yang mengambil token (atau api key) dari localStorage
// Kita menggunakan fungsi promise di header untuk memastikan token selalu up to date
const token = await getAuthToken();
return {
Authorization: 'Bearer ' + token,
...(requestHeaders || {}),
};
},
body: {
participantEmail: "user@mail.com",
},
});
const { error, status, sendMessage, messages, regenerate, stop } = useChat({
id: generateId(),
transport: networkTransport,
});
return (
<div className="flex flex-col w-full max-w-md py-12 mx-auto">
{messages.map(m => (
<div key={m.id} className="whitespace-pre-wrap">
{m.role === 'user' ? 'User: ' : 'AI: '}
{m.parts.map(part => part.type === 'text' ? part.text : '').join('')}
</div>
))}
{(status === 'submitted' || status === 'streaming') && (
<div className="mt-4 text-muted-foreground">
{status === 'submitted' && <div>Loading...</div>}
<button type="button" className="px-3 py-1 border rounded" onClick={stop}>Stop</button>
</div>
)}
{error && (
<div className="mt-4">
<div className="text-red-500">Terjadi kesalahan.</div>
<button type="button" className="px-3 py-1 border rounded" onClick={() => regenerate()}>
Coba lagi
</button>
</div>
)}
<form onSubmit={(e) => { e.preventDefault(); const form = e.currentTarget as HTMLFormElement; const input = (form.elements.namedItem('msg') as HTMLInputElement); if (!input.value) return; sendMessage({ text: input.value }); input.value=''; }}>
<input name="msg" className="mt-6 w-full p-2 border rounded" placeholder="Ketik sesuatu..." />
</form>
</div>
);
}SvelteKit (@ai-sdk/svelte)
Pertama, instal dependensi yang diperlukan:
npm install ai @ai-sdk/svelte
# atau
yarn add ai @ai-sdk/svelte
# atau
pnpm add ai @ai-sdk/svelteKemudian gunakan di komponen Anda:
<!-- src/routes/chat/+page.svelte -->
<script lang="ts">
import { Chat } from '@ai-sdk/svelte';
import { DefaultChatTransport, generateId } from 'ai';
const networkTransport = new DefaultChatTransport({
api: 'https://api.korinai.com/api/chat',
headers: async () => {
// Ambil token sesi/pengguna dari lapisan auth Anda (jangan expose API key di klien)
const token = await getAuthToken();
return {
Authorization: 'Bearer ' + token,
...(requestHeaders || {}),
};
},
body: {
participantEmail: "user@mail.com",
},
});
const chat = new Chat({ id: generateId(), transport: networkTransport });
let input = '';
const disabled = $derived(chat.status !== 'ready');
function handleSubmit(e: Event) { e.preventDefault(); chat.sendMessage({ text: input }); input=''; }
</script>
<div class="flex flex-col w-full max-w-md py-12 mx-auto">
{#each chat.messages as m (m.id)}
<div class="whitespace-pre-wrap">
{m.role === 'user' ? 'User: ' : 'AI: '}
{m.parts.map((p) => p.type === 'text' ? p.text : '').join('')}
</div>
{/each}
{#if chat.status === 'submitted' || chat.status === 'streaming'}
<div class="mt-4 text-gray-500">
{#if chat.status === 'submitted'}<div>Loading...</div>{/if}
<button type="button" class="px-3 py-1 border rounded" on:click={chat.stop}>Stop</button>
</div>
{/if}
{#if chat.error}
<div class="mt-4">
<div class="text-red-500">Terjadi kesalahan.</div>
<button type="button" class="px-3 py-1 border rounded" on:click={() => chat.regenerate()}>Coba lagi</button>
</div>
{/if}
<form on:submit|preventDefault={handleSubmit}>
<input bind:value={input} class="mt-6 w-full p-2 border rounded" placeholder="Ketik sesuatu..." disabled={disabled} />
</form>
<p>{chat.status}</p>
</div>Angular (@ai-sdk/angular)
Pertama, instal dependensi yang diperlukan:
npm install ai @ai-sdk/angular
# atau
yarn add ai @ai-sdk/angular
# atau
pnpm add ai @ai-sdk/angularKemudian gunakan di komponen Anda:
// chat.component.ts (standalone component)
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Chat } from '@ai-sdk/angular';
import { DefaultChatTransport, generateId } from 'ai';
const networkTransport = new DefaultChatTransport({
api: 'https://api.korinai.com/api/chat',
headers: async () => {
// Ambil token sesi/pengguna dari lapisan auth Anda (jangan expose API key di klien)
const token = await getAuthToken();
return {
Authorization: 'Bearer ' + token,
...(requestHeaders || {}),
};
},
body: {
participantEmail: "user@mail.com",
},
});
@Component({
selector: 'app-chat',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<div class="flex flex-col gap-3 max-w-xl">
<ul class="prose dark:prose-invert">
<li *ngFor="let m of chat.messages"><strong>{{ m.role }}:</strong> {{ m.content }}</li>
</ul>
<form [formGroup]="chatForm" (ngSubmit)="sendMessage()" class="flex gap-2">
<input class="flex-1" formControlName="userInput" placeholder="Ketik sesuatu" />
<button type="submit" [disabled]="chat.isLoading">Kirim</button>
</form>
</div>
`,
})
export class ChatComponent {
private fb = inject(FormBuilder);
public chat: Chat = new Chat({
id: generateId(),
transport: networkTransport,
});
chatForm: FormGroup = this.fb.group({ userInput: ['', Validators.required] });
sendMessage() {
if (this.chatForm.invalid) return;
const userInput = this.chatForm.value.userInput as string;
this.chatForm.reset();
this.chat.sendMessage(
{ text: userInput },
{
// Opsional: teruskan payload tambahan ke proxy server Anda
body: { selectedModel: 'gpt-4.1' },
},
);
}
}Nuxt (@ai-sdk/vue)
Pertama, instal dependensi yang diperlukan:
npm install ai @ai-sdk/vue
# atau
yarn add ai @ai-sdk/vue
# atau
pnpm add ai @ai-sdk/vueKemudian gunakan di komponen Anda:
<!-- pages/index.vue -->
<script setup lang="ts">
import { Chat } from '@ai-sdk/vue';
import { computed, ref } from 'vue';
import { DefaultChatTransport, generateId } from 'ai';
const networkTransport = new DefaultChatTransport({
api: 'https://api.korinai.com/api/chat',
headers: async () => {
// Ambil token sesi/pengguna dari lapisan auth Anda (jangan expose API key di klien)
const token = await getAuthToken();
return {
Authorization: 'Bearer ' + token,
...(requestHeaders || {}),
};
},
body: {
participantEmail: "user@mail.com",
},
});
const chat = new Chat({ id: generateId(), transport: networkTransport });
const input = ref('');
const disabled = computed(() => chat.status !== 'ready');
const handleSubmit = (e: Event) => { e.preventDefault(); chat.sendMessage({ text: input.value }); input.value=''; };
</script>
<template>
<div class="flex flex-col w-full max-w-md py-12 mx-auto">
<div v-for="m in chat.messages" :key="m.id" class="whitespace-pre-wrap">
{{ m.role === 'user' ? 'User: ' : 'AI: ' }}
{{ m.parts.map(part => (part.type === 'text' ? part.text : '')).join('') }}
</div>
<div v-if="chat.status === 'submitted' || chat.status === 'streaming'" class="mt-4 text-gray-500">
<div v-if="chat.status === 'submitted'">Loading...</div>
<button type="button" class="px-3 py-1 border rounded" @click="chat.stop">Stop</button>
</div>
<div v-if="chat.error" class="mt-4">
<div class="text-red-500">Terjadi kesalahan.</div>
<button type="button" class="px-3 py-1 border rounded" @click="() => chat.regenerate()">Coba lagi</button>
</div>
<form @submit="handleSubmit">
<input class="mt-6 w-full p-2 border rounded" v-model="input" placeholder="Ketik sesuatu..." :disabled="disabled" />
</form>
</div>
</template>Sisi Server
Di sisi server, proxy Anda menjaga rahasia tidak masuk ke klien, menyeragamkan header, dan menentukan cara mengirim hasil: alirkan SSE (stream‑through) untuk UI interaktif atau konsumsi di server (server‑consume) untuk menggabungkan pesan akhir asisten dan mengirim JSON. Validasi input, tegakkan auth/batas laju, atur CORS dengan benar, serta pasang timeout dan retry yang masuk akal. Catat request ID dari upstream dan alasan selesai untuk observabilitas. Pilih stream‑through untuk UX terbaik; pilih server‑consume untuk alur yang membutuhkan satu payload final.
Catatan: Body request harus menyertakan participantEmail dan messages.
- participantEmail adalah email peserta yang diajak chat. Gunakan email pengguna terautentikasi untuk chat ke diri sendiri; atau gunakan email pengguna lain untuk chat dengan pengguna/agen tersebut.
- Lihat Mulai Cepat untuk contoh request minimal.
Node.js (Express)
// server.js
import express from 'express';
import fetch from 'node-fetch';
const app = express();
app.use(express.json());
app.post('/api/chat', async (req, res) => {
// Teruskan permintaan ke upstream, lalu KONSUMSI SSE di server dan balikan JSON
const upstream = await fetch('https://api.korinai.com/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.KORIN_API_KEY}`,
},
body: JSON.stringify(req.body),
});
const reader = upstream.body?.getReader();
if (!reader) {
return res.status(502).json({ error: 'Upstream tidak mengembalikan body' });
}
const decoder = new TextDecoder();
let buffer = '';
let finalText = '';
let metadata: any = undefined;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const data = line.slice(6);
if (data === '[DONE]') break;
try {
const evt = JSON.parse(data);
if (evt.type === 'text-delta' && typeof evt.delta === 'string') {
finalText += evt.delta;
} else if (evt.type === 'message-metadata') {
metadata = evt.metadata;
}
} catch {}
}
}
return res.json({ text: finalText, metadata });
});
app.listen(3000, () => console.log('Mendengarkan di http://localhost:3000'));PHP (endpoint proxy)
<?php
// chat-proxy.php
// PENTING: Jangan pernah mengekspos API key Anda ke browser.
// Skrip PHP ini mem‑proxy permintaan di sisi server dan mengalirkan SSE kembali ke klien.
// Endpoint ini MENGONSUMSI SSE di server dan mengembalikan JSON (tanpa passthrough SSE)
header('Content-Type: application/json; charset=utf-8');
// Baca body JSON dari klien
$input = file_get_contents('php://input');
if ($input === false) {
http_response_code(400);
echo json_encode(['error' => 'Invalid request body']);
exit;
}
$ch = curl_init('https://api.korinai.com/api/chat');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . getenv('KORIN_API_KEY'),
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $input);
// Stream respons dan gabungkan teks asisten final
$finalText = '';
$metadata = null;
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $chunk) use (&$finalText, &$metadata) {
// Bagi menjadi baris dan parse data SSE
$lines = preg_split("/\r?\n/", $chunk);
foreach ($lines as $line) {
if (strpos($line, 'data: ') !== 0) continue;
$data = substr($line, 6);
if ($data === '[DONE]') continue;
$evt = json_decode($data, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($evt)) {
if (($evt['type'] ?? null) === 'text-delta' && isset($evt['delta'])) {
$finalText .= (string)$evt['delta'];
} elseif (($evt['type'] ?? null) === 'message-metadata' && isset($evt['metadata'])) {
$metadata = $evt['metadata'];
}
}
}
return strlen($chunk);
});
$ok = curl_exec($ch);
if ($ok === false) {
http_response_code(502);
echo json_encode(['error' => curl_error($ch)]);
curl_close($ch);
exit;
}
curl_close($ch);
echo json_encode(['text' => $finalText, 'metadata' => $metadata]);Praktik Terbaik
-
Keamanan (kunci & permukaan)
- Simpan kunci provider/API hanya di server. Jangan expose di bundle klien atau header.
- Gunakan environment variable untuk rahasia dan rotasi kunci terjadwal. Batasi hak akses kunci (least privilege).
- Validasi input di server (panjang, tipe, ukuran file) sebelum diteruskan ke upstream.
-
Streaming UX
- Tampilkan status mengetik/streaming dan sediakan tombol Stop selama generasi.
- Render pesan pengguna secara optimistis; alirkan respons asisten bertahap.
- Untuk jawaban panjang, auto‑scroll saat streaming dan jeda auto‑scroll saat pengguna menggulir ke atas.
-
Penanganan error & retry
- Bedakan error yang terlihat pengguna (401/403/429) vs error server sementara (5xx). Tampilkan pesan yang jelas dan membantu.
- Sertakan aksi Coba lagi; saat retry, pertimbangkan mengirim ulang dengan konteks room/percakapan yang sama.
- Catat request ID upstream (jika ada) untuk mengaitkan log klien dan server.
-
Batas laju & kredit
- Tegakkan kuota per pengguna/tenant di server sebelum memanggil upstream. Balas 403 dengan pesan yang membantu jika terlampaui.
- Lacak penggunaan per model/Tools untuk analitik dan kontrol biaya. Pertimbangkan batas lunak dengan peringatan.
-
Performa
- Stream‑through untuk UX interaktif; server‑consume untuk endpoint yang harus mengembalikan JSON ringkas.
- Hindari pekerjaan berat dalam loop baca SSE—tambahkan delta saja, jadwalkan pekerjaan berat setelah stream selesai.
- Gunakan HTTP keep‑alive dan kompresi bila cocok; nonaktifkan kompresi untuk SSE jika memperburuk latensi.
-
Orkestrasi & Tools
- Jaga peran sempit dan kompon alur (Riset → Draft → Review). Namai koneksi dengan jelas untuk audit.
- Wajibkan konfirmasi untuk Tools dengan efek samping atau panggilan jaringan; catat input/output Tools.
- Utamakan kemampuan di level adapter (status, stop, regenerate) daripada implementasi ulang kustom.
-
Landasan pengetahuan
- Jaga file/catatan kecil dan jelas; minta sitasi untuk meningkatkan kepercayaan.
- Bawaan: pencarian otomatis di pengetahuan yang diaktifkan; izinkan pembatasan ke sumber tertentu saat presisi dibutuhkan.
-
Observabilitas
- Catat siklus hidup stream: mulai, delta, selesai, batal, alasan selesai. Sertakan ID percakapan/room untuk keterlacakan.
- Tangkap metadata minimal untuk privasi (timestamp, request ID, model, durasi, jumlah token).
-
Pengujian & lingkungan
- Pisahkan agen staging vs production dan toggle Tools. Uji asap streaming, stop, dan retry sebelum rilis.
- Tambah uji integrasi yang mensimulasikan jaringan lambat dan output besar; pastikan UI tetap responsif.