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 (
);
}
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' ? (
) : m.markdown ? (
) : (
{m.content}
)}
{ m.type==='loading' ? `${Math.floor((Date.now()-m.start)/1000)}s` : m.time}
))}
{activeTool.id==='chatgpt' && (
)}
);
}
function DocTranslationSidebar() {
const [files, setFiles] = useState([]);
const extractRealFilename = (filename) => {
const regex = /^\[doc_translation\]_admin_\d{14,17}_(.+)$/;
const match = filename.match(regex);
return match ? match[1] : filename;
};
const loadFiles = async () => {
try {
const res = await fetch(`${API_BASE_URL}/doc_translation/files`);
const data = await res.json();
setFiles(Array.isArray(data) ? data : []);
} catch (_) {}
};
useEffect(() => { loadFiles(); }, []);
const handleUpload = async () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.doc,.docx';
input.multiple = true;
input.style.display = 'none';
document.body.appendChild(input);
input.onchange = async () => {
const files = Array.from(input.files || []);
if (!files.length) return;
const formData = new FormData();
files.forEach((f) => formData.append('files', f));
try {
await fetch(`${API_BASE_URL}/doc_translation/upload_doc`, { method: 'POST', body: formData });
} finally {
document.body.removeChild(input);
loadFiles();
}
};
input.click();
};
const handleDelete = async (serverFilename) => {
if (!window.confirm(`${extractRealFilename(serverFilename)} 파일을 삭제하시겠습니까?`)) return;
await fetch(`${API_BASE_URL}/doc_translation/files/${encodeURIComponent(serverFilename)}`, { method: 'DELETE' });
loadFiles();
};
const handleDownload = async (filename) => {
const res = await fetch(`${API_BASE_URL}/doc_translation/download/${encodeURIComponent(filename)}`);
if (!res.ok) return alert('다운로드 실패');
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = extractRealFilename(filename);
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
document.body.removeChild(a);
};
return (
파일 리스트
{files.map((f) => (
-
{f.has_result ? (
{extractRealFilename(f.filename)}
) : (
<>
번역중...
>
)}
))}
);
}
function DocTranslationChat() {
const { user } = useAuth();
const { selectedTool } = useTool();
const [messages, setMessages] = useState([]);
const [sending, setSending] = useState(false);
const [input, setInput] = useState('');
const [model, setModel] = useState('internal'); // internal | external
const inputRef2 = useRef(null);
const chatEndRef = useRef(null);
// 카드 전환 시 저장된 대화 불러오기
useEffect(() => {
try {
const saved = JSON.parse(localStorage.getItem('messages_doc_translation') || '[]');
setMessages(Array.isArray(saved) ? saved : []);
} catch { setMessages([]); }
}, [selectedTool?.id]);
useEffect(() => { chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
// 변경 시 저장
useEffect(() => {
try { localStorage.setItem('messages_doc_translation', JSON.stringify(messages)); } catch {}
}, [messages]);
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, { type: 'loading', start: Date.now() }]);
setInput('');
if (inputRef2.current) inputRef2.current.style.height = '32px';
try {
const form = new FormData();
form.append('message', userMsg.content);
// ChatPage 공통 /chat 엔드포인트의 문서번역 분기와 동일 동작을 위해 model 전달
form.append('tool_id', 'doc_translation');
form.append('model', model === 'internal' ? 'internal' : 'gpt-4o');
const res = await fetch(`${API_BASE_URL}/chat`, { method: 'POST', body: form });
const data = await res.json();
const botContent = data.response;
setMessages((prev) => {
const copy = [...prev];
const idx = copy.findIndex((m) => m.type === 'loading');
const botMsg = { type: 'bot', content: botContent, time: new Date().toLocaleTimeString(), markdown: true };
if (idx !== -1) copy[idx] = botMsg; else copy.push(botMsg);
return copy;
});
} catch (e) {
setMessages((prev) => [...prev, { type: 'bot', content: '서버 오류', time: new Date().toLocaleTimeString() }]);
}
setSending(false);
};
return (
<>
문서번역
MS Word 문서를 업로드하면 한글을 영어로 번역합니다.
{messages.map((m, idx) => (
{m.type==='loading' ? (
생각중...
) : m.markdown ? (
) : (
{m.content}
)}
{ m.type==='loading' ? `${Math.floor((Date.now()-m.start)/1000)}s` : m.time}
))}
>
);
}