'use client'; import { useEffect, useState, type FormEvent } from 'react'; import type { ChatMessage } from '@/lib/types'; interface ChatWindowProps { projectId: number; userName: string; } export function ChatWindow({ projectId, userName }: ChatWindowProps) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [error, setError] = useState(null); // 履歴取得 useEffect(() => { fetch(`/api/projects/${projectId}/chat/messages`) .then((res) => (res.ok ? res.json() : null)) .then((data) => { if (data?.items) setMessages(data.items as ChatMessage[]); }) .catch(() => undefined); }, [projectId]); // SSE接続(自動再接続はEventSourceが行う) useEffect(() => { const es = new EventSource(`/api/projects/${projectId}/chat/stream`); es.onmessage = (event) => { try { const parsed = JSON.parse(event.data) as { type: string; data: { message?: ChatMessage; id?: number }; }; if (parsed.type === 'chat.message.created' && parsed.data.message) { setMessages((prev) => prev.some((m) => m.id === parsed.data.message!.id) ? prev : [parsed.data.message!, ...prev] ); } else if ( parsed.type === 'chat.message.updated' && parsed.data.message ) { setMessages((prev) => prev.map((m) => m.id === parsed.data.message!.id ? parsed.data.message! : m ) ); } else if (parsed.type === 'chat.message.deleted' && parsed.data.id) { setMessages((prev) => prev.filter((m) => m.id !== parsed.data.id)); } } catch { // 無効なペイロードは無視 } }; return () => es.close(); }, [projectId]); async function onSend(event: FormEvent) { event.preventDefault(); if (!input.trim()) return; setError(null); const res = await fetch(`/api/projects/${projectId}/chat/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ body: input }), }); if (res.ok) { setInput(''); } else { const b = (await res.json().catch(() => null)) as { error?: { message?: string }; } | null; setError(b?.error?.message ?? '送信に失敗しました'); } } return (
    {messages.map((m) => (
  • {m.body} {m.createdAt}
  • ))}
setInput(e.target.value)} placeholder="メッセージを入力(@email でメンション)" className="flex-1 rounded border px-3 py-2" data-testid="chat-input" />
{error && (

{error}

)}
); }