import React, { useState, useEffect, useRef } from 'react'; import { useTool } from '../context/ToolContext'; import DevChatInput from '../tools/dev_chatbot/ChatInput.jsx'; import { marked } from 'marked'; marked.setOptions({ mangle:false, headerIds:false }); import { fetchOpenAIChat, fetchOpenAIImage, classifyRequest } from '../services/openaiService'; import { useAuth } from '../context/AuthContext'; import { API_BASE_URL } from '../config'; // === Chat History Panel 컴포넌트 추가 === function ChatHistoryPanel({ toolId }) { const [open, setOpen] = React.useState(false); const [messages, setMessages] = React.useState([]); // 패널이 열릴 때마다 로컬스토리지에서 메시지 불러오기 React.useEffect(() => { if (!open) return; try { const saved = JSON.parse(localStorage.getItem(`messages_${toolId}`) || '[]'); setMessages(Array.isArray(saved) ? saved : []); } catch { setMessages([]); } }, [toolId, open]); return ( <>
대화 히스토리
); } function FileSidebar() { const [files, setFiles] = useState([]); const loadFiles = async () => { try { const res = await fetch(`${API_BASE_URL}/files`); const data = await res.json(); setFiles(data.files || []); } catch (_) {} }; useEffect(() => { loadFiles(); }, []); const handleDelete = async (fname) => { if (!window.confirm(`${fname} 파일을 삭제하시겠습니까?`)) return; await fetch(`${API_BASE_URL}/file?filename=${encodeURIComponent(fname)}`, { method: 'DELETE' }); loadFiles(); }; const handleUpload = async (e) => { const files = Array.from(e.target.files || []); if (!files.length) return; const formData = new FormData(); files.forEach((f) => formData.append('files', f)); await fetch(`${API_BASE_URL}/upload_pdf`, { method: 'POST', body: formData }); loadFiles(); }; return (
파일 리스트
); } export default function ChatPage() { const { selectedTool } = useTool(); const { user } = useAuth(); const [messages, setMessages] = useState(() => { try { const toolIdInitial = selectedTool?.id || 'chatgpt'; const saved = JSON.parse(localStorage.getItem(`messages_${toolIdInitial}`) || '[]'); return Array.isArray(saved) ? saved.filter(m=>m.type!=='loading') : []; } catch { return []; } }); const [input, setInput] = useState(''); const [chatModel, setChatModel] = useState('GPT-5'); const inputRef = useRef(null); const chatEndRef = useRef(null); const [sending, setSending] = useState(false); // 기본 카드: ChatGPT (도구 목록에서 제거되었기 때문에 selectedTool 이 없으면 ChatGPT로 동작) const defaultChatTool = { id: 'chatgpt', name: 'Recipe Engine Demo', description: '영양/질환/약물 제약을 포함해 “설명 가능한” 레시피 추천을 만드는 데모입니다. (모델 선택 가능)', }; const activeTool = selectedTool || defaultChatTool; // 히스토리 패널용 공통 값 const toolId = selectedTool?.id || 'chatgpt'; const historyPanel = ; useEffect(() => { // 카드 전환 시 해당 카드의 저장된 대화 불러오기 (기본: chatgpt) const toolId = selectedTool?.id || 'chatgpt'; try { const key = `messages_${toolId}`; const saved = JSON.parse(localStorage.getItem(key) || '[]'); setMessages(Array.isArray(saved) ? saved.filter(m=>m.type!=='loading') : []); } catch { setMessages([]); } }, [selectedTool?.id]); useEffect(() => { // scroll to bottom chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); // 대화 변경 시 로컬스토리지에 저장 (기본: chatgpt) useEffect(() => { const toolId = selectedTool?.id || 'chatgpt'; try { localStorage.setItem(`messages_${toolId}`, JSON.stringify(messages)); } catch {} }, [messages, selectedTool?.id]); // (iframe으로 제공하는 도구는 더 이상 없음) if (selectedTool && selectedTool.id === 'dev_chatbot') { return (
{historyPanel} {/* DevChatInput renders header, messages, input */}
); } // 문서번역 도구: Word 파일 업로드/목록/다운로드/삭제 + 실시간 텍스트 번역 지원 if (selectedTool && selectedTool.id === 'doc_translation') { return (
{historyPanel}
); } const sendMessage = async () => { if (sending) return; if (!user) { alert('로그인이 필요합니다'); return; } setSending(true); if (!input.trim()) { setSending(false); return; } const userMsg = { type: 'user', content: input.trim(), time: new Date().toLocaleTimeString() }; setMessages((prev) => [...prev, userMsg]); setInput(''); if (inputRef.current) { inputRef.current.style.height = '32px'; } // loading placeholder const loadingMsg = { type: 'loading', start: Date.now() }; setMessages((prev) => [...prev, loadingMsg]); // interval to update elapsed seconds const loadingInterval = setInterval(() => { setMessages((prev) => { const last = prev[prev.length - 1]; if (last && last.type === 'loading') return [...prev]; // trigger rerender clearInterval(loadingInterval); return prev; }); }, 1000); // backend 호출 try { let botContent = ''; if (activeTool.id === 'chatgpt') { const modelMap = { 'GPT-5': 'gpt-5', 'GPT-5-mini': 'gpt-5-mini', 'GPT-5-nano': 'gpt-5-nano', 'GPT-4o': 'gpt-4o', 'GPT-4.1-mini': 'gpt-4o-mini', 'o4-mini': 'o4-mini', }; const model = modelMap[chatModel] || modelMap['GPT-5']; const sysPrompt = `당신은 한국어로 답하는 뛰어난 AI 어시스턴트입니다. ✅ 반드시 사실(팩트)에 기반하여 거짓 정보 없이 답하십시오. ✅ 먼저 내부적으로 생각(Chain-of-Thought)을 수행한 뒤, 이용자에게는 정리된 결과만 보여 주세요. ✅ 충분한 길이(최소 5문단)로, 필요 시 제목·소제목·순서/불릿 목록·코드 블록을 포함한 **Markdown** 형식을 사용하십시오. ✅ 이해를 돕기 위해 😃, 📌, 💡 등 적절한 이모지를 활용하십시오. ✅ 요약만 제시하지 말고, 예시·배경 설명·추가 정보를 상세히 제공합니다. ✅ 가능하다면 신뢰할 수 있는 공개 출처 URL 또는 참고문헌을 맨 아래에 제시하십시오.`; // 이미지 생성 의도 간단 감지 (예: 이미지/그려줘/생성해줘/그림/사진 등 키워드) // 1차: 키워드 룰기반, 2차: 모델 분류기로 보강 (샘플코드 방식) let wantsImage = /\b(이미지|그려줘|그림|사진|image|draw|generate\s+image|그려|create\s+an?\s+image|render)\b/i.test(userMsg.content); if (!wantsImage) { try { const cls = await classifyRequest(userMsg.content, 'gpt-5'); wantsImage = cls === 'image'; } catch (_) { /* ignore */ } } if (wantsImage) { try { const dataUrl = await fetchOpenAIImage(userMsg.content, 'gpt-image-1', { size: '1024x1024' }); // 이미지 응답은 전용 타입으로 렌더링 clearInterval(loadingInterval); const botMsg = { type: 'image', content: dataUrl, time: new Date().toLocaleTimeString() }; setMessages((prev) => { const copy = [...prev]; const idx = copy.findIndex((m) => m.type === 'loading'); if (idx !== -1) copy[idx] = botMsg; else copy.push(botMsg); return copy; }); return; // 조기 반환 (텍스트 처리 불필요) } catch (err) { // 이미지 생성 실패 시 텍스트 답변으로 폴백 botContent = await fetchOpenAIChat([ { role: 'system', content: sysPrompt }, { role: 'user', content: userMsg.content }, ], model, { }); } } else { botContent = await fetchOpenAIChat([ { role: 'system', content: sysPrompt }, { role: 'user', content: userMsg.content }, ], model, { }); } } else { const formData = new FormData(); formData.append('message', userMsg.content); formData.append('tool_id', activeTool.id); const res = await fetch(`${API_BASE_URL}/chat`, { method: 'POST', body: formData }); const data = await res.json(); botContent = data.response; } clearInterval(loadingInterval); const botMsg = { type: 'bot', content: botContent, time: new Date().toLocaleTimeString(), markdown:true }; // replace loading placeholder setMessages((prev) => { const copy = [...prev]; const idx = copy.findIndex((m) => m.type === 'loading'); if (idx !== -1) copy[idx] = botMsg; return copy; }); } catch (e) { setMessages((prev) => [...prev, { type: 'bot', content: '서버 오류', time: new Date().toLocaleTimeString() }]); } setSending(false); }; return (
{historyPanel}
{activeTool.name}
{activeTool.description}
{messages.map((m, idx) => (
{m.type==='loading' ? (
생각중...
) : m.type==='image' ? (
generated
) : m.markdown ? (
) : (
{m.content}
)}
{ m.type==='loading' ? `${Math.floor((Date.now()-m.start)/1000)}s` : m.time}
))}
{activeTool.id==='chatgpt' && ( )}