AIチャットインターフェイス
:LiTarget: 用途
AIアシスタント用のチャットUI。ストリーミング表示対応。
:LiSparkle: 特徴
- ストリーミング表示
- 履歴管理
- Markdown対応
- コードハイライト
:LiCode: コード(コピペ用)
'use client';
import { useState, useRef, useEffect } from 'react';
type Msg = { role: 'user' | 'assistant'; content: string };
// AI チャット UI(ストリーミング対応)
export function AIChat({
onSend,
}: {
onSend: (msgs: Msg[]) => AsyncIterable<string>; // SSE / fetch streaming
}) {
const [msgs, setMsgs] = useState<Msg[]>([]);
const [input, setInput] = useState('');
const [streaming, setStreaming] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' });
}, [msgs]);
const send = async () => {
if (!input.trim() || streaming) return;
const userMsg: Msg = { role: 'user', content: input.trim() };
const next = [...msgs, userMsg, { role: 'assistant' as const, content: '' }];
setMsgs(next);
setInput('');
setStreaming(true);
try {
let acc = '';
for await (const chunk of onSend([...msgs, userMsg])) {
acc += chunk;
setMsgs((m) => [...m.slice(0, -1), { role: 'assistant', content: acc }]);
}
} finally {
setStreaming(false);
}
};
return (
<div className="flex flex-col h-[600px] bg-zinc-900 rounded-2xl border border-zinc-800">
<div ref={scrollRef} className="flex-1 overflow-y-auto p-4 space-y-3">
{msgs.map((m, i) => (
<div key={i} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-[80%] rounded-2xl px-4 py-2 ${m.role === 'user' ? 'bg-indigo-500 text-white' : 'bg-zinc-800 text-zinc-100'}`}>
<pre className="whitespace-pre-wrap font-sans text-sm">{m.content || (streaming && i === msgs.length - 1 ? '...' : '')}</pre>
</div>
</div>
))}
</div>
<div className="border-t border-zinc-800 p-3 flex gap-2">
<input value={input} onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } }}
disabled={streaming}
placeholder="メッセージを入力..."
className="flex-1 bg-zinc-800 border border-zinc-700 rounded-xl px-4 py-2 text-sm text-zinc-100 focus:outline-none focus:border-indigo-500" />
<button onClick={send} disabled={streaming || !input.trim()}
className="px-4 py-2 rounded-xl bg-indigo-500 text-white disabled:opacity-50">送信</button>
</div>
</div>
);
}
:LiHandPointer: 使い方
対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。
:LiAlertCircle: 注意事項
- 依存パッケージを忘れず追加