Files
sam-sales/ref/components/Assistant.tsx
2025-12-17 13:25:40 +09:00

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;