232 lines
13 KiB
PHP
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>
|