import { useState, useRef, useEffect } from 'react' import { View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet, KeyboardAvoidingView, Platform, ActivityIndicator, } from 'react-native' import { router } from 'expo-router' import { COLORS, API_BASE } from '../../constants/Config' import { sendAIMessage } from '../../services/api' import { useAuth } from '../../hooks/useAuth' import { authFetch } from '../../utils/auth' import LineIcon from '../../components/LineIcon' import { VoiceInput } from '../../components/VoiceInput' import { NextActions } from '../../components/NextActions' import { generateJSON, DEFAULT_TEXT_MODEL } from '../../lib/ollama' interface Msg { id: number; role: 'user' | 'ai'; text: string; time: string } // Ollama가 자연어를 분류하는 명령 스키마 interface Command { intent: 'query_server' | 'create_sr' | 'query_sr' | 'open_screen' | 'chat' server_id?: string screen?: string reply?: string } const QUICK = ['서버 상태 확인', 'SR 목록 보여줘', '최근 인시던트', '긴급 SR 등록'] export default function ChatScreen() { const { user } = useAuth() const [msgs, setMsgs] = useState([ { id: 0, role: 'ai', text: `안녕하세요 ${user?.display_name ?? ''}님! 👋\nGUARDiA AI 어시스턴트입니다.\n자연어로 명령하시면 SR 등록·조회를 자동 실행합니다.`, time: now() }, ]) const [input, setInput] = useState('') const [loading, setLoading] = useState(false) const [lastContext, setLastContext] = useState('AI 챗봇 대기 중') const scrollRef = useRef(null) function now() { return new Date().toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' }) } function pushAI(text: string) { setMsgs(m => [...m, { id: Date.now() + Math.random(), role: 'ai', text, time: now() }]) } // 자연어 → Ollama 의도 분류 → 명령 실행 async function classifyAndRun(text: string): Promise { const prompt = `당신은 ITSM 운영 어시스턴트입니다. 운영자 입력: "${text}". ` + `의도를 JSON으로만 분류하세요: ` + `{"intent":"query_server|create_sr|query_sr|open_screen|chat",` + `"server_id":"서버ID(있으면)","screen":"sr|dr|network|notifications(있으면)","reply":"chat일 때 한국어 답변"}` const cmd = await generateJSON(DEFAULT_TEXT_MODEL, prompt, { intent: 'chat', reply: '' }) switch (cmd.intent) { case 'query_server': { if (!cmd.server_id) return '조회할 서버를 알려주세요. (예: "서버 001 상태")' try { const res = await authFetch(`${API_BASE}/api/servers/${encodeURIComponent(cmd.server_id)}`) if (res.ok) { const d = await res.json() // 보안: ip/ssh/pw 등 민감 필드는 표시하지 않음 const name = d.name ?? d.hostname ?? cmd.server_id const status = d.status ?? d.state ?? '알 수 없음' return `🖥️ 서버 ${name}\n상태: ${status}${d.cpu ? `\nCPU: ${d.cpu}%` : ''}${d.mem ? `\n메모리: ${d.mem}%` : ''}` } return `서버 ${cmd.server_id} 정보를 가져오지 못했습니다.` } catch { return '서버 조회 중 오류가 발생했습니다.' } } case 'create_sr': setTimeout(() => router.push('/sr'), 400) return '📋 SR 등록 화면을 엽니다.' case 'query_sr': setTimeout(() => router.push('/sr'), 400) return '📋 SR 목록 화면으로 이동합니다.' case 'open_screen': { const screen = cmd.screen ?? 'index' setTimeout(() => router.push(`/${screen}` as any), 400) return `화면(${screen})으로 이동합니다.` } default: return cmd.reply || '' } } const send = async (text = input) => { if (!text.trim() || loading) return const trimmed = text.trim() setMsgs(m => [...m, { id: Date.now(), role: 'user', text: trimmed, time: now() }]) setInput('') setLoading(true) setLastContext(`사용자 요청: ${trimmed}`) try { // 1) Ollama 의도 분류 + 명령 실행 const local = await classifyAndRun(trimmed) if (local) { pushAI(local) } else { // 2) 폴백: ITSM 챗봇 API const r = await sendAIMessage(trimmed) const reply = r.data?.reply ?? r.data?.message ?? r.data?.response ?? '응답을 받았습니다.' pushAI(reply) } } catch { pushAI('현재 AI 서버에 연결할 수 없습니다. Ollama 서버 상태를 확인해주세요.') } finally { setLoading(false) } setTimeout(() => scrollRef.current?.scrollToEnd({ animated: true }), 100) } useEffect(() => { setTimeout(() => scrollRef.current?.scrollToEnd({ animated: false }), 50) }, [msgs]) return ( {msgs.map(m => ( {m.role === 'ai' && ( )} {m.text} {m.time} ))} {loading && ( )} {/* #20 다음 명령 제안 */} {!loading && ( send(a)} /> )} {QUICK.map(q => ( send(q)}> {q} ))} {/* #25 음성 입력 → 입력창 자동 전달 (ko-KR) */} { if (t) setInput(t) }} size="small" /> send()} /> send()} disabled={!input.trim() || loading}> ) } const s = StyleSheet.create({ container: { flex: 1, backgroundColor: COLORS.bg }, messages: { flex: 1 }, msgRow: { flexDirection: 'row', alignItems: 'flex-end', marginBottom: 12, gap: 8 }, userRow: { flexDirection: 'row-reverse' }, avatar: { width: 38, height: 38, alignItems: 'center', justifyContent: 'center', marginBottom: 4 }, bubble: { maxWidth: '75%', borderRadius: 16, padding: 12 }, aiBubble: { backgroundColor: '#fff', borderBottomLeftRadius: 4, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: .06, elevation: 1 }, userBubble: { backgroundColor: COLORS.accent, borderBottomRightRadius: 4 }, bubbleText: { fontSize: 14, color: COLORS.text, lineHeight: 20 }, userText: { color: '#fff' }, timeText: { fontSize: 10, color: COLORS.muted, marginTop: 4, textAlign: 'right' }, quickScroll: { maxHeight: 44, borderTopWidth: 1, borderTopColor: COLORS.border, backgroundColor: '#fff' }, quickChip: { alignSelf: 'center', backgroundColor: COLORS.light, paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20 }, quickChipText: { fontSize: 12, color: COLORS.accent, fontWeight: '500' }, inputRow: { flexDirection: 'row', padding: 12, backgroundColor: '#fff', borderTopWidth: 1, borderTopColor: COLORS.border, alignItems: 'flex-end', gap: 8 }, textInput: { flex: 1, borderWidth: 1.5, borderColor: COLORS.border, borderRadius: 20, paddingHorizontal: 14, paddingVertical: 10, fontSize: 14, color: COLORS.text, maxHeight: 100, backgroundColor: '#fafafa' }, sendBtn: { width: 42, height: 42, borderRadius: 21, backgroundColor: COLORS.accent, justifyContent: 'center', alignItems: 'center' }, sendDisabled: { opacity: .4 }, })