@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
104 lines (101 loc) • 7.97 kB
JavaScript
"use client";
import { jsxs, jsx } from 'react/jsx-runtime';
import { useState } from 'react';
import { Button } from '../ui/Button.js';
import { Card, CardHeader, CardContent } from '../ui/Card.js';
// 답변에서 링크를 버튼으로 렌더링하는 함수
function renderAnswerWithLinks(text) {
if (!text)
return text;
// 네이버 블로그 링크 패턴 감지
const linkPattern = /(https?:\/\/blog\.naver\.com\/[^\s\n]+)/g;
// 링크가 실제로 존재하는지 확인
if (!/(https?:\/\/blog\.naver\.com\/[^\s\n]+)/.test(text)) {
return text;
}
const parts = text.split(linkPattern);
return 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-3 py-1 bg-green-100 text-green-700 rounded-full text-sm 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;
});
}
function RAGAnswerGenerator({ apiEndpoint = "/api/rag/answer/stream", placeholder = "질문을 입력하세요... (예: 회사 소개, 제품 정보 등)", maxResults = 5, }) {
const [query, setQuery] = useState("");
const [answer, setAnswer] = useState("");
const [sources, setSources] = useState([]);
const [searchResults, setSearchResults] = useState([]);
const [loading, setLoading] = useState(false);
const [showDetails, setShowDetails] = useState(false);
const handleSearch = async () => {
var _a;
if (!query.trim())
return;
try {
setLoading(true);
setAnswer("");
setSources([]);
setSearchResults([]);
const response = await fetch(apiEndpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, k: maxResults }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
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 = "";
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") {
setSources(data.content || []);
}
else if (data.type === "content") {
currentAnswer += data.content;
setAnswer(currentAnswer);
}
else if (data.type === "error") {
setAnswer(`오류: ${data.content}`);
break;
}
}
catch (e) {
// JSON 파싱 오류 무시
}
}
}
}
}
catch (error) {
console.error("RAG 답변 생성 중 오류:", error);
setAnswer("네트워크 오류가 발생했습니다.");
}
finally {
setLoading(false);
}
};
return (jsxs("div", { className: "space-y-6", children: [jsxs("div", { className: "flex gap-2", children: [jsx("input", { value: query, onChange: (e) => setQuery(e.target.value), placeholder: placeholder, className: "flex-1 h-10 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent", onKeyDown: (e) => e.key === "Enter" && handleSearch() }), jsx(Button, { onClick: handleSearch, disabled: loading || !query.trim(), loading: loading, children: loading ? "답변 생성 중..." : "질문하기" })] }), answer && (jsxs(Card, { children: [jsx(CardHeader, { children: jsxs("div", { className: "flex items-center justify-between", children: [jsx("h3", { className: "text-lg font-semibold text-gray-900", children: "\uD83E\uDD16 AI \uB2F5\uBCC0" }), sources.length > 0 && (jsxs("span", { className: "text-sm text-gray-500", children: [sources.length, "\uAC1C \uBB38\uC11C \uCC38\uC870"] }))] }) }), jsxs(CardContent, { children: [jsx("div", { className: "prose max-w-none", children: jsx("div", { className: "text-gray-800 leading-relaxed whitespace-pre-wrap", children: renderAnswerWithLinks(answer) }) }), sources.filter((source) => !source.isRSS).length > 0 && (jsxs("div", { className: "mt-6 pt-4 border-t border-gray-200", children: [jsx("h4", { className: "text-sm font-medium text-gray-700 mb-3", children: "\uD83D\uDCDA \uCC38\uC870 \uBB38\uC11C:" }), jsx("div", { className: "space-y-2", children: sources
.filter((source) => !source.isRSS)
.map((source, index) => (jsxs("div", { className: "flex items-center gap-2 text-sm", children: [jsx("span", { className: "bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs font-medium", children: source.index }), jsx("div", { className: "flex-1", children: jsx("span", { className: "text-gray-700 truncate block max-w-md", children: source.title }) }), jsxs("span", { className: "text-gray-500 text-xs", children: [(source.score * 100).toFixed(0), "%"] })] }, index))) })] })), searchResults.length > 0 && (jsxs("div", { className: "mt-4", children: [jsx(Button, { variant: "ghost", size: "sm", onClick: () => setShowDetails(!showDetails), children: showDetails ? "상세 정보 숨기기" : "검색된 청크 보기" }), showDetails && (jsxs("div", { className: "mt-4 space-y-3", children: [jsx("h4", { className: "text-sm font-medium text-gray-700", children: "\uD83D\uDD0D \uAC80\uC0C9\uB41C \uCCAD\uD06C\uB4E4:" }), searchResults.map((result, index) => {
var _a, _b, _c;
return (jsxs("div", { className: "p-3 bg-gray-50 rounded border-l-4 border-blue-200", children: [jsxs("div", { className: "text-xs text-gray-600 mb-2", children: ["\uC810\uC218: ", (_a = result.score) === null || _a === void 0 ? void 0 : _a.toFixed(3), " | \uCD9C\uCC98:", " ", (_b = result.document) === null || _b === void 0 ? void 0 : _b.source] }), jsx("div", { className: "text-sm text-gray-800", children: ((_c = result.chunk) === null || _c === void 0 ? void 0 : _c.content) || result.content })] }, index));
})] }))] }))] })] })), loading && (jsxs("div", { className: "text-center py-8", children: [jsx("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2" }), jsx("p", { className: "text-gray-600", children: "AI\uAC00 \uB2F5\uBCC0\uC744 \uC0DD\uC131\uD558\uACE0 \uC788\uC2B5\uB2C8\uB2E4..." })] }))] }));
}
export { RAGAnswerGenerator };
//# sourceMappingURL=rag-answer-generator.js.map