Files
sam-kd/chatbot/md_rag/index.php
hskwon aca1767eb9 초기 커밋: 5130 레거시 시스템
- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경
- DB 연결 하드코딩 → .env 기반으로 변경
- MySQL strict mode DATE 오류 수정
2025-12-10 20:14:31 +09:00

232 lines
13 KiB
PHP

<?php
// chatbot/md_rag/index.php
require_once($_SERVER['DOCUMENT_ROOT'] . "/session.php");
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>테넌트 지식관리 챗봇 - codebridge-x.com</title>
<!-- Fonts: Pretendard -->
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.8/dist/web/static/pretendard.css" />
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Pretendard', 'sans-serif'],
},
colors: {
background: 'rgb(250, 250, 250)',
primary: {
DEFAULT: '#059669', // emerald-600 (Green for Tenant)
foreground: '#ffffff',
},
},
animation: {
'blob': 'blob 7s infinite',
'fade-in-up': 'fadeInUp 0.5s ease-out forwards',
},
keyframes: {
blob: {
'0%': { transform: 'translate(0px, 0px) scale(1)' },
'33%': { transform: 'translate(30px, -50px) scale(1.1)' },
'66%': { transform: 'translate(-20px, 20px) scale(0.9)' },
'100%': { transform: 'translate(0px, 0px) scale(1)' },
},
fadeInUp: {
'0%': { opacity: '0', transform: 'translateY(10px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
}
}
}
}
}
</script>
<style>
.scrollbar-hide::-webkit-scrollbar { display: none; }
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
</style>
<!-- React & ReactDOM -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!-- Babel for JSX -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- Icons: Lucide React -->
<script src="https://unpkg.com/lucide@latest"></script>
</head>
<body class="bg-background text-slate-800 antialiased overflow-hidden">
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useRef } = React;
const Sender = { USER: 'user', BOT: 'bot' };
// Point to local api.php
const sendMessageToGemini = async (userMessage, history = []) => {
try {
const response = await fetch('api.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: userMessage, history: history }),
});
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
// Debug log (Hidden from UI but visible in console for admin)
if (data.debug) console.log("Debug:", data.debug);
return data.reply || "죄송합니다. 응답을 받을 수 없습니다.";
} catch (error) {
console.error("API Error:", error);
return "오류가 발생했습니다. 잠시 후 다시 시도해주세요.";
}
};
// Icons
const ChatMultipleIcon = ({ className }) => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4c0-1.1.9-2 2-2h8a2 2 0 0 1 2 2v5Z"/><path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1"/></svg>
);
const CloseIcon = ({ className }) => (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className={className}><path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
);
const SendIcon = ({ className }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}><path d="M3.478 2.405a.75.75 0 00-.926.94l2.432 7.905H13.5a.75.75 0 010 1.5H4.984l-2.432 7.905a.75.75 0 00.926.94 60.519 60.519 0 0018.445-8.986.75.75 0 000-1.218A60.517 60.517 0 003.478 2.405z" /></svg>
);
// Header
const Header = () => (
<header className="bg-white border-b border-gray-100 sticky top-0 z-40">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<div className="flex items-center gap-3">
<h1 className="text-lg font-semibold text-slate-900">테넌트 지식관리 챗봇</h1>
</div>
<div className="flex items-center gap-4">
<a href="/" className="text-sm text-slate-500 hover:text-slate-900 flex items-center gap-1">
<i data-lucide="home" className="w-4 h-4"></i> 홈으로
</a>
<a href="upload.php" className="text-sm text-emerald-600 hover:text-emerald-800 flex items-center gap-1 font-medium">
<i data-lucide="upload" className="w-4 h-4"></i> 지식 관리
</a>
</div>
</div>
</header>
);
// Chat Components
const BotAvatar = ({ size = 'md', className = '' }) => {
const sizeClasses = { sm: 'w-8 h-8 text-xs', md: 'w-10 h-10 text-sm' };
return <div className={`${sizeClasses[size]} rounded-full bg-gradient-to-br from-emerald-500 to-teal-600 flex items-center justify-center text-white font-bold shadow-sm ${className}`}>Help</div>;
};
const ChatWindow = () => {
const [messages, setMessages] = useState([{
id: 'welcome',
text: "안녕하세요! 테넌트 서비스 이용 중 궁금한 점이 있으신가요? 📄\n업로드된 지식 문서를 바탕으로 답변해드립니다.",
sender: Sender.BOT,
timestamp: new Date(),
}]);
const [inputText, setInputText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef(null);
useEffect(() => messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }), [messages]);
const handleSendMessage = async (e) => {
e?.preventDefault();
if (!inputText.trim() || isLoading) return;
const userMsg = { id: Date.now().toString(), text: inputText, sender: Sender.USER };
setMessages(prev => [...prev, userMsg]);
setInputText('');
setIsLoading(true);
try {
const history = messages.slice(-5).map(msg => ({
role: msg.sender === Sender.USER ? 'user' : 'model',
parts: [{ text: msg.text }]
}));
const responseText = await sendMessageToGemini(userMsg.text, history);
setMessages(prev => [...prev, { id: 'bot-'+Date.now(), text: responseText, sender: Sender.BOT }]);
} catch (error) { console.error(error); }
finally { setIsLoading(false); }
};
return (
<div className="flex flex-col h-full w-full bg-white rounded-lg overflow-hidden shadow-2xl font-sans">
<div className="bg-emerald-600 p-4 pt-6 pb-6 text-white shrink-0 relative">
<div className="flex items-center space-x-3">
<BotAvatar size="md" className="border-2 border-emerald-400" />
<div>
<h2 className="font-bold text-lg leading-tight">Tenant Support Bot</h2>
<p className="text-xs text-emerald-100 opacity-90 mt-0.5">지식 문서 기반 AI 상담원</p>
</div>
</div>
</div>
<div className="flex-1 overflow-y-auto p-4 bg-gray-50 space-y-4 scrollbar-hide">
{messages.map((msg) => (
<div key={msg.id} className={`flex ${msg.sender === Sender.USER ? 'justify-end' : 'justify-start'}`}>
{msg.sender === Sender.BOT && <div className="mr-2 mt-1 flex-shrink-0"><BotAvatar size="sm" /></div>}
<div className={`max-w-[80%] rounded-2xl px-4 py-3 text-sm leading-relaxed shadow-sm whitespace-pre-wrap ${
msg.sender === Sender.USER ? 'bg-emerald-600 text-white rounded-tr-none' : 'bg-white text-gray-800 border border-gray-100 rounded-tl-none'
}`}>{msg.text}</div>
</div>
))}
{isLoading && <div className="flex justify-start animate-pulse"><div className="mr-2 mt-1"><BotAvatar size="sm" /></div><div className="bg-white text-gray-400 border border-gray-100 rounded-2xl px-4 py-3 text-sm shadow-sm">답변 찾는 중...</div></div>}
<div ref={messagesEndRef} />
</div>
<div className="p-4 bg-white border-t border-gray-100 shrink-0">
<form onSubmit={handleSendMessage} className="flex items-center w-full border border-gray-300 rounded-full px-4 py-2 focus-within:border-emerald-600 focus-within:ring-1 focus-within:ring-emerald-600 transition-all shadow-sm">
<input type="text" className="flex-1 outline-none text-sm bg-transparent" placeholder="질문을 입력하세요..." value={inputText} onChange={(e) => setInputText(e.target.value)} />
<button type="submit" disabled={!inputText.trim() || isLoading} className={`ml-2 p-1 ${!inputText.trim() ? 'text-gray-300' : 'text-emerald-600'}`}><SendIcon className="w-5 h-5" /></button>
</form>
</div>
</div>
);
};
const ChatWidget = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="fixed bottom-6 right-6 z-50 flex flex-col items-end gap-3 font-sans">
<div className={`transition-all duration-300 ease-in-out origin-bottom-right ${isOpen ? 'scale-100 opacity-100' : 'scale-90 opacity-0 pointer-events-none absolute bottom-16 right-0'} w-[380px] h-[600px] max-w-[calc(100vw-48px)] max-h-[calc(100vh-120px)]`}>
<ChatWindow />
</div>
<button onClick={() => setIsOpen(!isOpen)} className={`w-16 h-16 rounded-full shadow-xl flex items-center justify-center transition-all duration-300 ${isOpen ? 'bg-emerald-600 rotate-90' : 'bg-emerald-600 hover:bg-emerald-700 hover:scale-105'}`}>
{isOpen ? <CloseIcon className="w-8 h-8 text-white" /> : <ChatMultipleIcon className="w-8 h-8 text-white" />}
</button>
</div>
);
};
const App = () => {
useEffect(() => lucide.createIcons(), []);
return (
<div className="w-full min-h-screen relative bg-gray-50">
<Header />
<div className="w-full h-full flex items-center justify-center p-20 min-h-[80vh]">
<div className="text-center opacity-30 select-none">
<h1 className="text-6xl font-bold text-gray-300 mb-8">Tenant Knowledge Base</h1>
<p className="text-2xl text-gray-400">MD File Based RAG System</p>
</div>
<div className="absolute left-10 top-1/2 w-96 h-96 bg-emerald-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob"></div>
<div className="absolute right-10 top-1/3 w-96 h-96 bg-teal-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob animation-delay-2000"></div>
</div>
<ChatWidget />
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>