Create streaming chat UIs in React that can switch between GPT-5.2, Claude, Gemini, and 6 more models. One API, one hook, infinite possibilities.
You only pay credits per request. No monthly subscription. Paid credits never expire.
Replace multiple AI subscriptions with one wallet that includes routing, failover, and optimization.
npm install llmwise
// hooks/useChat.ts — Custom React hook for streaming LLMWise chat
import { useState, useCallback, useRef } from "react";
interface Message {
id: string;
role: "user" | "assistant";
content: string;
}
interface UseChatOptions {
model?: string;
apiUrl?: string;
}
export function useChat({ model = "auto", apiUrl = "/api/chat" }: UseChatOptions = {}) {
const [messages, setMessages] = useState<Message[]>([]);
const [isLoading, setIsLoading] = useState(false);
const abortRef = useRef<AbortController | null>(null);
const sendMessage = useCallback(async (content: string) => {
const userMsg: Message = { id: crypto.randomUUID(), role: "user", content };
const assistantMsg: Message = { id: crypto.randomUUID(), role: "assistant", content: "" };
setMessages((prev) => [...prev, userMsg, assistantMsg]);
setIsLoading(true);
abortRef.current = new AbortController();
try {
const res = await fetch(apiUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model,
messages: [...messages, userMsg].map(({ role, content }) => ({ role, content })),
stream: true,
}),
signal: abortRef.current.signal,
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let idx = 0;
while ((idx = buffer.indexOf("\n\n")) !== -1) {
const raw = buffer.slice(0, idx);
buffer = buffer.slice(idx + 2);
for (const line of raw.split("\n")) {
if (!line.startsWith("data: ")) continue;
const data = line.slice(6);
if (data === "[DONE]") return;
const parsed = JSON.parse(data);
if (parsed.error) throw new Error(parsed.error);
const delta = parsed.delta ?? "";
if (delta) {
setMessages((prev) =>
prev.map((m) =>
m.id === assistantMsg.id ? { ...m, content: m.content + delta } : m
)
);
}
if (parsed.event === "done") return;
}
}
}
} catch (err) {
if ((err as Error).name !== "AbortError") console.error(err);
} finally {
setIsLoading(false);
}
}, [messages, model, apiUrl]);
const stop = useCallback(() => abortRef.current?.abort(), []);
return { messages, sendMessage, isLoading, stop };
}
// ChatApp.tsx — Example component using the hook
import { useState } from "react";
import { useChat } from "./hooks/useChat";
const MODELS = ["auto", "gpt-5.2", "claude-sonnet-4.5", "gemini-3-flash", "deepseek-v3"];
export function ChatApp() {
const [model, setModel] = useState("auto");
const [input, setInput] = useState("");
const { messages, sendMessage, isLoading, stop } = useChat({ model });
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
sendMessage(input);
setInput("");
};
return (
<div style={{ maxWidth: 640, margin: "0 auto", padding: 16 }}>
<select value={model} onChange={(e) => setModel(e.target.value)}>
{MODELS.map((m) => <option key={m} value={m}>{m}</option>)}
</select>
<div style={{ minHeight: 400, overflowY: "auto" }}>
{messages.map((m) => (
<div key={m.id} style={{ marginBottom: 12 }}>
<strong>{m.role === "user" ? "You" : model}:</strong>
<p>{m.content}</p>
</div>
))}
</div>
<form onSubmit={handleSubmit} style={{ display: "flex", gap: 8 }}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
style={{ flex: 1, padding: 8 }}
/>
{isLoading ? (
<button type="button" onClick={stop}>Stop</button>
) : (
<button type="submit">Send</button>
)}
</form>
</div>
);
}Everything you need to integrate LLMWise's multi-model API into your React project.
Never expose your LLMWise API key in client-side code. Create a thin backend endpoint (Express, Fastify, or any server) that forwards requests to LLMWise and streams the response back to the browser.
// server.ts — Minimal Express proxy
import express from "express";
import { LLMWise } from "llmwise";
const app = express();
app.use(express.json());
const client = new LLMWise(process.env.LLMWISE_API_KEY!);
app.post("/api/chat", async (req, res) => {
const { messages, model = "auto" } = req.body;
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
for await (const ev of client.chatStream({ model, messages })) {
res.write("data: " + JSON.stringify(ev) + "\n\n");
if (ev.event === "done") break;
if ((ev as any).error) break;
}
res.write("data: [DONE]\n\n");
res.end();
});
app.listen(3001);Build a React hook that manages message state, streams responses using the Fetch API ReadableStream, and supports aborting in-flight requests. This gives you full control without depending on framework-specific libraries.
// See the full useChat hook in the code example above. // Key features: message state, SSE parsing, AbortController, model parameter.
Use the useChat hook in a React component. Add a model selector to let users switch between models, a message list with auto-scroll, and a form with a submit handler.
import { useChat } from "./hooks/useChat";
export function Chat() {
const { messages, sendMessage, isLoading } = useChat({ model: "claude-sonnet-4.5" });
const [input, setInput] = useState("");
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
<form onSubmit={(e) => { e.preventDefault(); sendMessage(input); setInput(""); }}>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button disabled={isLoading}>Send</button>
</form>
</div>
);
}Store the selected model in component state and pass it to the useChat hook. When the user picks a different model, the next message will be routed to that model automatically.
const [model, setModel] = useState("gpt-5.2");
const { messages, sendMessage } = useChat({ model });
// Model selector dropdown
<select value={model} onChange={(e) => setModel(e.target.value)}>
<option value="gpt-5.2">GPT-5.2</option>
<option value="claude-sonnet-4.5">Claude Sonnet 4.5</option>
<option value="gemini-3-flash">Gemini 3 Flash</option>
<option value="deepseek-v3">DeepSeek V3</option>
</select>The useChat hook exposes isLoading and stop(). Use isLoading to show a typing indicator or disable the input, and wire stop() to a cancel button so users can abort long-running generations.
const { messages, sendMessage, isLoading, stop } = useChat({ model });
// Show loading indicator
{isLoading && <div className="typing-indicator">Generating...</div>}
// Cancel button
{isLoading && <button onClick={stop}>Cancel</button>}You only pay credits per request. No monthly subscription. Paid credits never expire.
Replace multiple AI subscriptions with one wallet that includes routing, failover, and optimization.