UNPKG

@restnfeel/agentc-starter-kit

Version:

한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템

266 lines (263 loc) 16 kB
import { jsxs, Fragment, jsx } from 'react/jsx-runtime'; import { useState, useEffect } from 'react'; import { Button } from '../ui/Button.js'; import { cn } from '../../utils/cn.js'; // 답변에서 링크를 버튼으로 렌더링하는 함수 function renderAnswerWithLinks(text) { if (!text) return jsx("span", { children: text }); // 네이버 블로그 링크 패턴 감지 const linkPattern = /(https?:\/\/blog\.naver\.com\/[^\s\n]+)/g; // 링크가 실제로 존재하는지 확인 if (!/(https?:\/\/blog\.naver\.com\/[^\s\n]+)/.test(text)) { return jsx("span", { children: text }); } const parts = text.split(linkPattern); return (jsx("span", { children: parts.map((part, index) => { if (part.match(linkPattern)) { return (jsxs("a", { href: part, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-1 mx-1 px-2 py-1 bg-green-100 text-green-700 rounded-full text-xs hover:bg-green-200 transition-colors font-medium", title: "\uC6D0\uBB38 \uBCF4\uAE30", children: [jsx("svg", { className: "w-3 h-3", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" }) }), "\uD83D\uDD17 \uC6D0\uBB38 \uBCF4\uAE30"] }, index)); } return part; }) })); } // 마크다운 링크를 JSX로 변환하는 함수 function renderMessageContent(content) { const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; const parts = []; let lastIndex = 0; let match; while ((match = linkRegex.exec(content)) !== null) { // 링크 이전 텍스트 추가 if (match.index > lastIndex) { parts.push(content.slice(lastIndex, match.index)); } // 링크 추가 parts.push(jsx("a", { href: match[2], target: "_blank", rel: "noopener noreferrer", className: "text-blue-600 hover:text-blue-800 underline", children: match[1] }, match.index)); lastIndex = match.index + match[0].length; } // 남은 텍스트 추가 if (lastIndex < content.length) { parts.push(content.slice(lastIndex)); } return parts.length > 1 ? parts : content; } function ChatWidget({ agentConfigEndpoint = "/api/rag/agent-config", chatEndpoint = "/api/rag/answer/stream", buttonColor = "#ef4444", position = "bottom-right", generateFallbackResponse, }) { const [isOpen, setIsOpen] = useState(false); const [inputValue, setInputValue] = useState(""); const [agentConfig, setAgentConfig] = useState({ name: "RAG 어시스턴트", description: "문서 기반 질문 답변 AI 어시스턴트", greeting: "안녕하세요! 업로드된 문서를 기반으로 질문에 답변해드립니다. 궁금한 것이 있으시면 언제든 물어보세요!", personality: "친근하고 도움이 되는", updatedAt: new Date().toISOString(), }); const [messages, setMessages] = useState([]); // 에이전트 설정 로드 const loadAgentConfig = async () => { try { const response = await fetch(agentConfigEndpoint); const data = await response.json(); if (data.success) { setAgentConfig(data.config); // 초기 인사말 메시지 설정 setMessages([ { id: "1", content: data.config.greeting, sender: "ai", timestamp: new Date(), type: "text", }, ]); } } catch (error) { console.error("에이전트 설정 로드 실패:", error); // 기본 인사말 사용 setMessages([ { id: "1", content: agentConfig.greeting, sender: "ai", timestamp: new Date(), type: "text", }, ]); } }; // 컴포넌트 마운트 시 에이전트 설정 로드 useEffect(() => { loadAgentConfig(); }, []); // 기본 폴백 응답 생성 함수 const defaultFallbackResponse = (userInput) => { const input = userInput.toLowerCase(); if (input.includes("안녕") || input.includes("hello")) { return "안녕하세요! 현재 문서 검색에 문제가 있어 기본 응답을 드립니다. 관리자에게 문의해주세요."; } else if (input.includes("agentc") || input.includes("에이전트")) { return "AgentC는 AI 기반의 스마트 CMS 시스템입니다. 현재 문서 검색 기능에 문제가 있어 자세한 정보를 제공하지 못하고 있습니다."; } else { return `죄송합니다. 현재 문서 검색 시스템에 문제가 발생하여 "${userInput}"에 대한 정확한 답변을 드릴 수 없습니다.\n\n시스템이 복구되면 다시 시도해주세요. 또는 관리자에게 문의해주세요.`; } }; // 메시지 전송 핸들러 const handleSendMessage = async () => { var _a; if (!inputValue.trim()) return; const userQuery = inputValue; // 사용자 메시지 추가 const userMessage = { id: Date.now().toString(), content: userQuery, sender: "user", timestamp: new Date(), type: "text", }; setMessages((prev) => [...prev, userMessage]); setInputValue(""); // 로딩 메시지 추가 const loadingMessage = { id: (Date.now() + 1).toString(), content: "답변을 생성하고 있습니다...", sender: "ai", timestamp: new Date(), type: "system", }; setMessages((prev) => [...prev, loadingMessage]); try { // RAG 스트리밍 API 호출 const response = await fetch(chatEndpoint, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ query: userQuery, k: 5, }), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // 로딩 메시지 제거 setMessages((prev) => prev.filter((msg) => msg.id !== loadingMessage.id)); // AI 응답 메시지 생성 (빈 내용으로 시작) const aiResponseId = (Date.now() + 2).toString(); const aiResponse = { id: aiResponseId, content: "", sender: "ai", timestamp: new Date(), type: "text", }; setMessages((prev) => [...prev, aiResponse]); // 스트리밍 처리 const reader = (_a = response.body) === null || _a === void 0 ? void 0 : _a.getReader(); if (!reader) { throw new Error("스트림을 읽을 수 없습니다."); } const decoder = new TextDecoder(); let buffer = ""; let currentAnswer = ""; let sources = []; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (line.trim()) { try { const data = JSON.parse(line); if (data.type === "sources") { sources = data.content || []; } else if (data.type === "content") { currentAnswer += data.content; // 실시간으로 메시지 업데이트 setMessages((prev) => prev.map((msg) => msg.id === aiResponseId ? { ...msg, content: currentAnswer } : msg)); } else if (data.type === "error") { setMessages((prev) => prev.map((msg) => msg.id === aiResponseId ? { ...msg, content: `오류: ${data.content}`, type: "error", } : msg)); break; } } catch (e) { // JSON 파싱 오류 무시 } } } } // 소스 정보가 있으면 추가 (RSS가 아닌 문서만) const nonRSSSources = sources.filter((source) => !source.isRSS); if (nonRSSSources.length > 0) { const sourcesText = `\n\n📚 **참고 문서:**\n${nonRSSSources .map((source) => `• ${source.title} (${Math.round(source.score * 100)}%)`) .join("\n")}`; const sourcesMessage = { id: (Date.now() + 3).toString(), content: sourcesText, sender: "ai", timestamp: new Date(), type: "system", }; setMessages((prev) => [...prev, sourcesMessage]); } } catch (error) { console.error("RAG 요청 실패:", error); // 로딩 메시지 제거 setMessages((prev) => prev.filter((msg) => msg.id !== loadingMessage.id)); // 폴백 응답 const fallbackMessage = { id: (Date.now() + 2).toString(), content: generateFallbackResponse ? generateFallbackResponse(userQuery) : defaultFallbackResponse(userQuery), sender: "ai", timestamp: new Date(), type: "text", }; setMessages((prev) => [...prev, fallbackMessage]); } }; // 엔터키 핸들러 const handleKeyPress = (e) => { if (e.key === "Enter") { handleSendMessage(); } }; const positionClasses = { "bottom-right": "bottom-6 right-6", "bottom-left": "bottom-6 left-6", }; return (jsxs(Fragment, { children: [jsx("div", { className: cn("fixed z-50", positionClasses[position]), children: jsx("button", { onClick: () => setIsOpen(!isOpen), className: "w-16 h-16 text-white rounded-full shadow-2xl transition-all duration-200 hover:scale-110 border-4 border-white flex items-center justify-center", style: { backgroundColor: buttonColor, boxShadow: "0 10px 25px rgba(0,0,0,0.3)", }, "aria-label": "\uCC44\uD305 \uC5F4\uAE30", children: isOpen ? (jsx("svg", { className: "w-8 h-8", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })) : (jsx("svg", { className: "w-8 h-8", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" }) })) }) }), isOpen && (jsxs("div", { className: cn("fixed z-50 bg-white flex flex-col", "md:w-96 md:h-96 md:rounded-2xl md:shadow-2xl md:border md:border-gray-200", "max-md:inset-0 max-md:w-screen max-md:h-screen max-md:rounded-none max-md:top-0 max-md:left-0 max-md:right-0 max-md:bottom-0", position === "bottom-right" ? "md:bottom-24 md:right-6" : "md:bottom-24 md:left-6"), children: [jsxs("div", { className: "flex items-center justify-between p-4 border-b border-gray-200 bg-gray-50 md:rounded-t-2xl max-md:sticky max-md:top-0 max-md:z-10", children: [jsxs("div", { className: "flex items-center gap-3", children: [jsx("button", { onClick: () => setIsOpen(false), className: "md:hidden w-8 h-8 rounded-full hover:bg-gray-200 flex items-center justify-center transition-colors", "aria-label": "\uB4A4\uB85C\uAC00\uAE30", children: jsx("svg", { className: "w-5 h-5 text-gray-600", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) }) }), jsxs("div", { children: [jsx("h3", { className: "font-semibold text-gray-800", children: agentConfig.name }), agentConfig.description && (jsx("p", { className: "text-xs text-gray-500", children: agentConfig.description }))] })] }), jsx("button", { onClick: () => setIsOpen(false), className: "hidden md:flex w-8 h-8 rounded-full hover:bg-gray-200 items-center justify-center transition-colors", "aria-label": "\uB2EB\uAE30", children: jsx("svg", { className: "w-5 h-5 text-gray-600", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] }), jsx("div", { className: "flex-1 overflow-y-auto p-4 space-y-3 max-md:pb-20", children: messages.map((message) => (jsx("div", { className: cn("flex", message.sender === "user" ? "justify-end" : "justify-start"), children: jsxs("div", { className: cn("px-3 py-2 rounded-lg text-sm max-w-xs md:max-w-xs max-md:max-w-[80%]", message.sender === "user" ? "bg-blue-500 text-white" : message.type === "system" ? "bg-blue-50 text-blue-700 border border-blue-200" : message.type === "error" ? "bg-red-50 text-red-700 border border-red-200" : "bg-gray-200 text-gray-800"), children: [jsx("div", { className: "whitespace-pre-wrap", children: message.sender === "ai" && message.type === "text" ? renderAnswerWithLinks(message.content) : renderMessageContent(message.content) }), message.type === "system" && (jsx("div", { className: "text-xs text-blue-500 mt-1", children: message.content.includes("답변을 생성") ? "🤖" : "📚" }))] }) }, message.id))) }), jsx("div", { className: "p-4 border-t border-gray-200 bg-white max-md:fixed max-md:bottom-0 max-md:left-0 max-md:right-0 max-md:border-t-2", children: jsxs("div", { className: "flex gap-2", children: [jsx("input", { type: "text", value: inputValue, onChange: (e) => setInputValue(e.target.value), onKeyPress: handleKeyPress, placeholder: "\uBA54\uC2DC\uC9C0\uB97C \uC785\uB825\uD558\uC138\uC694...", className: "flex-1 p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-base" }), jsx(Button, { onClick: handleSendMessage, disabled: !inputValue.trim(), className: "px-4 py-3 min-w-[60px]", children: "\uC804\uC1A1" })] }) })] }))] })); } export { ChatWidget }; //# sourceMappingURL=chat-widget.js.map