@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
266 lines (263 loc) • 16 kB
JavaScript
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