import { useState, useRef, useEffect, useCallback } from 'react'; import type { Ship } from '../../types'; interface ChatMessage { role: 'user' | 'assistant' | 'system'; content: string; timestamp: number; } interface Props { ships: Ship[]; koreanShipCount: number; chineseShipCount: number; totalShipCount: number; } // TODO: Python FastAPI 기반 해양분석 AI API로 전환 예정 const AI_CHAT_URL = '/api/kcg/ai/chat'; function buildSystemPrompt(props: Props): string { const { ships, koreanShipCount, chineseShipCount, totalShipCount } = props; // 선박 유형별 통계 const byType: Record = {}; const byFlag: Record = {}; ships.forEach(s => { byType[s.category || 'unknown'] = (byType[s.category || 'unknown'] || 0) + 1; byFlag[s.flag || 'unknown'] = (byFlag[s.flag || 'unknown'] || 0) + 1; }); // 중국 어선 통계 const cnFishing = ships.filter(s => s.flag === 'CN' && (s.category === 'fishing' || s.typecode === '30')); const cnFishingOperating = cnFishing.filter(s => s.speed >= 2 && s.speed <= 5); return `당신은 대한민국 해양경찰청의 해양상황 분석 AI 어시스턴트입니다. 현재 실시간 해양 모니터링 데이터를 기반으로 분석을 제공합니다. ## 현재 해양 상황 요약 - 전체 선박: ${totalShipCount}척 - 한국 선박: ${koreanShipCount}척 - 중국 선박: ${chineseShipCount}척 - 중국 어선: ${cnFishing.length}척 (조업 추정: ${cnFishingOperating.length}척) ## 선박 유형별 현황 ${Object.entries(byType).map(([k, v]) => `- ${k}: ${v}척`).join('\n')} ## 국적별 현황 (상위) ${Object.entries(byFlag).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([k, v]) => `- ${k}: ${v}척`).join('\n')} ## 한중어업협정 핵심 - 중국 허가어선 906척 (PT 저인망 323쌍, GN 유자망 200척, PS 위망 16척, OT 1척식 13척, FC 운반선 31척) - 특정어업수역 I~IV에서만 조업 허가 - 휴어기: PT/OT 4/16~10/15, GN 6/2~8/31 - 다크베셀(AIS 차단) 감시 필수 ## 응답 규칙 - 한국어로 답변 - 간결하고 분석적으로 - 데이터 기반 답변 우선 - 불법조업 의심 시 근거 제시 - 필요시 조치 권고 포함`; } export function AiChatPanel({ ships, koreanShipCount, chineseShipCount, totalShipCount }: Props) { const [isOpen, setIsOpen] = useState(true); const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const messagesEndRef = useRef(null); const inputRef = useRef(null); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); useEffect(() => { if (isOpen) inputRef.current?.focus(); }, [isOpen]); const sendMessage = useCallback(async () => { if (!input.trim() || isLoading) return; const userMsg: ChatMessage = { role: 'user', content: input.trim(), timestamp: Date.now() }; setMessages(prev => [...prev, userMsg]); setInput(''); setIsLoading(true); try { const systemPrompt = buildSystemPrompt({ ships, koreanShipCount, chineseShipCount, totalShipCount }); const apiMessages = [ { role: 'system', content: systemPrompt }, ...messages.filter(m => m.role !== 'system').map(m => ({ role: m.role, content: m.content })), { role: 'user', content: userMsg.content }, ]; const res = await fetch(AI_CHAT_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'qwen2.5:7b', messages: apiMessages, stream: false, options: { temperature: 0.3, num_predict: 1024 }, }), }); if (!res.ok) throw new Error(`Ollama error: ${res.status}`); const data = await res.json(); const assistantMsg: ChatMessage = { role: 'assistant', content: data.message?.content || '응답을 생성할 수 없습니다.', timestamp: Date.now(), }; setMessages(prev => [...prev, assistantMsg]); } catch (err) { setMessages(prev => [...prev, { role: 'assistant', content: `오류: ${err instanceof Error ? err.message : 'AI 서버 연결 실패'}. Ollama가 실행 중인지 확인하세요.`, timestamp: Date.now(), }]); } finally { setIsLoading(false); } }, [input, isLoading, messages, ships, koreanShipCount, chineseShipCount, totalShipCount]); const quickQuestions = [ '현재 해양 상황을 요약해줘', '중국어선 불법조업 의심 분석해줘', '서해 위험도를 평가해줘', '다크베셀 현황 분석해줘', ]; return (
{/* Toggle header */}
setIsOpen(p => !p)} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '6px 8px', cursor: 'pointer', background: isOpen ? 'rgba(168,85,247,0.12)' : 'rgba(168,85,247,0.04)', borderRadius: 4, borderLeft: '2px solid rgba(168,85,247,0.5)', }} > 🤖 AI 해양분석 Qwen 2.5 {isOpen ? '▼' : '▶'}
{/* Chat body */} {isOpen && (
{/* Messages */}
{messages.length === 0 && (
해양 상황에 대해 질문하세요
{quickQuestions.map((q, i) => ( ))}
)} {messages.map((msg, i) => (
{msg.content}
))} {isLoading && (
분석 중...
)}
{/* Input */}
setInput(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }} placeholder="해양 상황 질문..." disabled={isLoading} style={{ flex: 1, background: 'rgba(139,92,246,0.06)', border: '1px solid rgba(139,92,246,0.2)', borderRadius: 4, padding: '5px 8px', fontSize: 10, color: '#e2e8f0', outline: 'none', }} />
)}
); }