160 lines
5.7 KiB
TypeScript
160 lines
5.7 KiB
TypeScript
import React, { useState, useRef, useEffect } from 'react';
|
|
import { GoogleGenAI } from "@google/genai";
|
|
import { Message } from '../types';
|
|
import { SYSTEM_INSTRUCTION } from '../constants';
|
|
import { MessageSquare, X, Send, Loader2, Sparkles } from 'lucide-react';
|
|
|
|
const Assistant: React.FC = () => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [messages, setMessages] = useState<Message[]>([
|
|
{ role: 'model', text: '안녕하십니까! CodeBridgeX 영업 지원 AI입니다. CEO 설득을 위한 핵심 스크립트나 자료가 필요하신가요?' }
|
|
]);
|
|
const [input, setInput] = useState('');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Auto-scroll to bottom
|
|
useEffect(() => {
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
}, [messages, isOpen]);
|
|
|
|
const handleSend = async () => {
|
|
if (!input.trim() || isLoading) return;
|
|
|
|
const userMessage = input.trim();
|
|
setInput('');
|
|
setMessages(prev => [...prev, { role: 'user', text: userMessage }]);
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const apiKey = process.env.API_KEY;
|
|
if (!apiKey) {
|
|
throw new Error("API Key is missing");
|
|
}
|
|
|
|
const ai = new GoogleGenAI({ apiKey });
|
|
|
|
// Build history for context
|
|
const history = messages.map(m => ({
|
|
role: m.role,
|
|
parts: [{ text: m.text }]
|
|
}));
|
|
|
|
const chat = ai.chats.create({
|
|
model: 'gemini-2.5-flash',
|
|
config: {
|
|
systemInstruction: SYSTEM_INSTRUCTION,
|
|
},
|
|
history: history
|
|
});
|
|
|
|
const result = await chat.sendMessageStream({ message: userMessage });
|
|
|
|
let fullResponse = '';
|
|
setMessages(prev => [...prev, { role: 'model', text: '' }]); // Placeholder
|
|
|
|
for await (const chunk of result) {
|
|
const text = chunk.text;
|
|
if (text) {
|
|
fullResponse += text;
|
|
setMessages(prev => {
|
|
const newMsgs = [...prev];
|
|
newMsgs[newMsgs.length - 1].text = fullResponse;
|
|
return newMsgs;
|
|
});
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error("AI Error:", error);
|
|
setMessages(prev => [...prev, { role: 'model', text: '죄송합니다. 일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' }]);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Floating Action Button */}
|
|
<button
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className={`fixed bottom-6 right-6 z-50 p-4 rounded-full shadow-2xl transition-all duration-300 hover:scale-105 ${
|
|
isOpen ? 'bg-slate-800 text-white rotate-90' : 'bg-brand-600 text-white'
|
|
}`}
|
|
>
|
|
{isOpen ? <X size={24} /> : <MessageSquare size={24} />}
|
|
</button>
|
|
|
|
{/* Chat Window */}
|
|
<div
|
|
className={`fixed bottom-24 right-6 z-40 w-[90vw] md:w-[380px] bg-white rounded-2xl shadow-2xl border border-slate-200 overflow-hidden flex flex-col transition-all duration-300 origin-bottom-right transform ${
|
|
isOpen ? 'scale-100 opacity-100 translate-y-0' : 'scale-90 opacity-0 translate-y-10 pointer-events-none'
|
|
}`}
|
|
style={{ height: '600px', maxHeight: '75vh' }}
|
|
>
|
|
{/* Header */}
|
|
<div className="bg-slate-900 p-4 flex items-center gap-3">
|
|
<div className="bg-brand-500 p-2 rounded-lg">
|
|
<Sparkles size={18} className="text-white" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-white font-bold text-sm">SAM Sales Assistant</h3>
|
|
<p className="text-slate-400 text-xs">Powered by Gemini 2.5 Flash</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Messages */}
|
|
<div className="flex-1 overflow-y-auto p-4 bg-slate-50 space-y-4">
|
|
{messages.map((msg, idx) => (
|
|
<div
|
|
key={idx}
|
|
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
|
>
|
|
<div
|
|
className={`max-w-[85%] p-3 rounded-2xl text-sm leading-relaxed ${
|
|
msg.role === 'user'
|
|
? 'bg-brand-600 text-white rounded-tr-none'
|
|
: 'bg-white text-slate-800 border border-slate-200 rounded-tl-none shadow-sm'
|
|
}`}
|
|
>
|
|
{msg.text}
|
|
</div>
|
|
</div>
|
|
))}
|
|
{isLoading && (
|
|
<div className="flex justify-start">
|
|
<div className="bg-white p-3 rounded-2xl rounded-tl-none border border-slate-200 shadow-sm">
|
|
<Loader2 size={16} className="animate-spin text-brand-500" />
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div ref={messagesEndRef} />
|
|
</div>
|
|
|
|
{/* Input Area */}
|
|
<div className="p-3 bg-white border-t border-slate-100">
|
|
<div className="flex items-center gap-2 bg-slate-100 rounded-full px-4 py-2 border border-transparent focus-within:border-brand-300 focus-within:bg-white transition-colors">
|
|
<input
|
|
type="text"
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
|
|
placeholder="제안 스크립트를 요청하세요..."
|
|
className="flex-1 bg-transparent outline-none text-sm text-slate-800 placeholder:text-slate-400"
|
|
disabled={isLoading}
|
|
/>
|
|
<button
|
|
onClick={handleSend}
|
|
disabled={isLoading || !input.trim()}
|
|
className="text-brand-600 hover:text-brand-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<Send size={18} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default Assistant; |