@neuradex/sdk/react는 Librarian / Chatbot 채팅 기능을 React 애플리케이션에 통합하기 위한 훅과 리치 메시지 타입을 제공합니다.
단순한 텍스트뿐만 아니라, 메시지 종류에 따라 검색 결과 카드, 지식 상세 표시, 에피소드 목록, 지식 변경 제안 승인 UI 등 풍부한 UI를 구축할 수 있습니다.
import { useChat } from '@neuradex/sdk/react' ;
import type { ChatMessage , RichMessageMetadata } from '@neuradex/sdk/react' ;
기본 사용법
function ChatUI () {
const {
messages ,
isStreaming ,
toolUse ,
pendingProposal ,
sendMessage ,
loadSessions ,
startNewSession ,
approveProposal ,
rejectProposal ,
editProposal ,
} = useChat ({
feature: 'librarian' ,
projectId: 'your-project-id' ,
endpoint: 'https://api.neuradex.ai/projects/your-project-id/librarian' ,
getAuthHeaders : async () => ({
Authorization: `Bearer ${ await getToken () } ` ,
}),
onToolUse : ( tool ) => console . log ( `실행 중: ${ tool . name } ` ),
onError : ( error ) => console . error ( error ),
});
return (
< div >
{ messages . map (( msg ) => (
< MessageRenderer key = { msg . id } message = { msg } />
)) }
{ toolUse && < ToolIndicator tool = { toolUse } /> }
</ div >
);
}
리치 메시지 시스템
useChat의 메시지는 일반 텍스트뿐만 아니라, role과 metadata.type 조합으로 다양한 UI 요소를 표현합니다.
메시지 역할
role 설명 user사용자 입력 assistantAI 응답 (텍스트, Markdown) notification검색 결과, 지식 상세 등의 리치 UI interactive_action사용자 조작이 필요한 제안 UI tool도구 실행 정보
메타데이터 타입
notification과 interactive_action 역할의 메시지는 metadata 필드에 리치 UI 정보를 가집니다.
metadata.type 설명 UI 예시 search_result시맨틱 검색 결과 검색 쿼리, 히트 수, 결과 목록 knowledge_view지식 상세 표시 제목, 콘텐츠, 태그, 관련 지식 knowledge_result지식 작업 완료 알림 추가/수정/삭제 성공 표시 knowledge_proposal지식 변경 제안 승인·거부·수정 인터랙티브 UI episode_query_result에피소드 검색 결과 검색 쿼리, 시간 범위, 결과 목록 episode_list_result에피소드 목록 요약, 질문/답변 수, 결과 목록
메시지 렌더링
message.role과 message.metadata?.type에 따라 UI 컴포넌트를 전환합니다.
function MessageRenderer ({ message } : { message : ChatMessage }) {
// 검색 결과
if ( message . metadata ?. type === 'search_result' ) {
const meta = message . metadata ;
return (
< SearchResultCard
query = { meta . query }
totalCount = { meta . totalCount }
results = { meta . results }
/>
);
}
// 지식 상세
if ( message . metadata ?. type === 'knowledge_view' ) {
const meta = message . metadata ;
return (
< KnowledgeViewCard
title = { meta . title }
content = { meta . content }
tags = { meta . tags }
connectedKnowledge = { meta . connectedKnowledge }
/>
);
}
// 지식 작업 결과
if ( message . metadata ?. type === 'knowledge_result' ) {
const meta = message . metadata ;
return (
< KnowledgeResultCard
actionType = { meta . actionType }
title = { meta . title }
knowledgeId = { meta . knowledgeId }
/>
);
}
// 지식 변경 제안 (인터랙티브)
if ( message . role === 'interactive_action'
&& message . metadata ?. type === 'knowledge_proposal' ) {
return < ProposalCard message = { message } /> ;
}
// 에피소드 관련
if ( message . metadata ?. type === 'episode_query_result'
|| message . metadata ?. type === 'episode_list_result' ) {
return < EpisodeResultCard metadata = { message . metadata } /> ;
}
// 일반 메시지
return (
< div className = { message . role === 'user' ? 'user-bubble' : 'assistant-bubble' } >
{ message . content }
</ div >
);
}
검색 결과 카드 (search_result)
지식 검색이 수행되면 결과가 구조화된 데이터로 반환됩니다.
import type { SearchResultPayload , SearchResultItem } from '@neuradex/sdk/react' ;
function SearchResultCard ({ query , totalCount , results } : SearchResultPayload ) {
return (
< div className = "search-result-card" >
< div className = "header" >
< SearchIcon />
< span > " { query } " 검색 결과 ( { totalCount } 건) </ span >
</ div >
< ul >
{ results . map (( item : SearchResultItem ) => (
< li key = { item . id } >
< a href = { `/knowledge/ ${ item . id } ` } > { item . title } </ a >
{ item . tags ?. map (( tag ) => < span className = "tag" > # { tag } </ span > ) }
</ li >
)) }
</ ul >
</ div >
);
}
지식 상세 카드 (knowledge_view)
특정 지식의 상세와 관련 지식이 표시됩니다.
import type { KnowledgeViewPayload , ConnectedKnowledgeItem } from '@neuradex/sdk/react' ;
function KnowledgeViewCard ({ title , content , tags , connectedKnowledge } : KnowledgeViewPayload ) {
return (
< div className = "knowledge-view-card" >
< div className = "header" >
< EyeIcon />
< a href = { `/knowledge/ ${ knowledgeId } ` } > { title } </ a >
{ connectedKnowledge . length > 0 && (
< span > + { connectedKnowledge . length } 건 </ span >
) }
</ div >
< p className = "content-preview" > { content } </ p >
< div className = "tags" >
{ tags . map (( tag ) => < span className = "tag" > # { tag } </ span > ) }
</ div >
{ connectedKnowledge . length > 0 && (
< div className = "connected" >
< LinkIcon /> 관련 ( { connectedKnowledge . length } 건)
{ connectedKnowledge . slice ( 0 , 3 ). map (( item : ConnectedKnowledgeItem ) => (
< a key = { item . edgeId } href = { `/knowledge/ ${ item . id } ` } >
{ item . title }
</ a >
)) }
</ div >
) }
</ div >
);
}
지식 작업 결과 (knowledge_result)
추가·수정·삭제 작업 완료를 표시합니다.
import type { KnowledgeResultPayload } from '@neuradex/sdk/react' ;
function KnowledgeResultCard ({ actionType , title , knowledgeId } : KnowledgeResultPayload ) {
const labels = { add: '추가' , update: '수정' , delete: '삭제' };
return (
< div className = { `result-card result- ${ actionType } ` } >
< span > 지식 " { title } "을(를) { labels [ actionType ] } 했습니다 </ span >
{ actionType !== 'delete' && (
< a href = { `/knowledge/ ${ knowledgeId } ` } > 상세 보기 </ a >
) }
</ div >
);
}
지식 변경 제안 (knowledge_proposal)
Librarian 은 지식 추가, 수정, 삭제를 제안 형태로 반환할 수 있습니다. 사용자가 승인·거부·수정하여 응답하는 인터랙티브 UI입니다.
import type { KnowledgeProposalPayload } from '@neuradex/sdk/react' ;
function ProposalCard ({ message } : { message : ChatMessage }) {
const meta = message . metadata as KnowledgeProposalPayload ;
const isPending = meta . userResponse === null ;
const [ isEditing , setIsEditing ] = useState ( false );
const [ editedContent , setEditedContent ] = useState ( meta . content || '' );
const { approveProposal , rejectProposal , editProposal } = useChat ({ /* ... */ });
return (
< div className = "proposal-card" >
{ /* 액션 배지 (추가/수정/삭제) */ }
< div className = "badge" > { meta . actionType } </ div >
{ /* 상태 표시 */ }
< h3 >
{ isPending ? '이 작업을 실행하시겠습니까?'
: meta . userResponse === 'approve' ? '승인됨'
: meta . userResponse === 'reject' ? '거부됨'
: '수정하여 실행됨' }
</ h3 >
{ /* 제안 이유 */ }
{ meta . reason && < p > { meta . reason } </ p > }
{ /* 제안 내용 */ }
< div className = "proposal-content" >
< span className = "title" > { meta . title } </ span >
{ isEditing ? (
< textarea
value = { editedContent }
onChange = { ( e ) => setEditedContent ( e . target . value ) }
/>
) : (
< p > { meta . content } </ p >
) }
{ meta . tags ?. map (( tag ) => < span className = "tag" > # { tag } </ span > ) }
</ div >
{ /* 작업 버튼 (미응답인 경우만) */ }
{ isPending && (
< div className = "actions" >
{ ! isEditing ? (
<>
< button onClick = { approveProposal } > 승인 </ button >
< button onClick = { rejectProposal } > 거부 </ button >
{ meta . actionType !== 'delete' && (
< button onClick = { () => setIsEditing ( true ) } > 수정 </ button >
) }
</>
) : (
<>
< button onClick = { () => editProposal ( editedContent ) } >
이 내용으로 실행
</ button >
< button onClick = { () => setIsEditing ( false ) } > 취소 </ button >
</>
) }
</ div >
) }
</ div >
);
}
도구 실행 인디케이터
toolUse 상태로 AI가 실행 중인 도구를 표시할 수 있습니다. ToolUseInfo는 범용적인 name과 args를 제공하므로, Neuradex 내장 도구뿐만 아니라 사용자가 정의한 커스텀 도구 도 동일한 패턴으로 처리할 수 있습니다.
import type { ToolUseInfo } from '@neuradex/sdk/react' ;
function ToolIndicator ({ tool } : { tool : ToolUseInfo }) {
switch ( tool . name ) {
// --- Neuradex 내장 도구 ---
case 'search_knowledge' :
return (
< div className = "tool-indicator purple animate-pulse" >
< SearchIcon />
{ tool . args ?. query
? `" ${ tool . args . query } " 검색 중...`
: '검색 중...' }
</ div >
);
case 'query_episodes' :
return (
< div className = "tool-indicator amber animate-pulse" >
< MessageIcon />
Q&A 이력 검색 중...
</ div >
);
case 'list_recent_episodes' :
return (
< div className = "tool-indicator teal animate-pulse" >
< ClockIcon />
최근 에피소드 불러오는 중...
</ div >
);
// --- 커스텀 도구 ---
case 'check_inventory' :
return (
< div className = "tool-indicator green animate-pulse" >
< BoxIcon />
" { tool . args ?. productName } " 재고 확인 중...
</ div >
);
case 'create_ticket' :
return (
< div className = "tool-indicator orange animate-pulse" >
< TicketIcon />
지원 티켓 생성 중...
</ div >
);
default :
return (
< div className = "tool-indicator blue animate-pulse" >
< SpinnerIcon />
{ tool . name } 실행 중...
</ div >
);
}
}
tool.name은 단순한 string이고 tool.args는 범용적인 Record<string, unknown>이므로, 백엔드가 제공하는 모든 도구(재고 확인, 티켓 생성, 외부 API 호출 등)를 동일한 패턴으로 처리할 수 있습니다. 도구 이름별로 case를 추가하기만 하면 됩니다.
에피소드 결과
Q&A 이력 검색 결과와 목록도 리치 UI로 표시할 수 있습니다.
import type {
EpisodeQueryResultPayload ,
EpisodeListResultPayload ,
EpisodeResultItem ,
} from '@neuradex/sdk/react' ;
function EpisodeResultCard ({ metadata } : { metadata : RichMessageMetadata }) {
if ( metadata . type === 'episode_list_result' ) {
const meta = metadata as EpisodeListResultPayload ;
return (
< div className = "episode-card" >
< p > { meta . summary } </ p >
< div className = "counts" >
질문: { meta . questionCount } 건 / 답변: { meta . answerCount } 건
</ div >
{ meta . results . map (( ep : EpisodeResultItem ) => (
< div key = { ep . id } className = { `episode-item episode- ${ ep . type } ` } >
< span className = "actor" > { ep . actor } </ span >
< span className = "content" > { ep . content } </ span >
< span className = "time" > { ep . occurredAt } </ span >
</ div >
)) }
</ div >
);
}
if ( metadata . type === 'episode_query_result' ) {
const meta = metadata as EpisodeQueryResultPayload ;
return (
< div className = "episode-card" >
< p > " { meta . query } " 검색 결과 ( { meta . total } 건) </ p >
{ meta . results . map (( ep : EpisodeResultItem ) => (
< div key = { ep . id } className = { `episode-item episode- ${ ep . type } ` } >
< span className = "actor" > { ep . actor } </ span >
< span className = "content" > { ep . content } </ span >
</ div >
)) }
</ div >
);
}
return null ;
}
UseChatOptions
feature
'librarian' | 'chatbot'
필수
기능 타입
onToolUse
(tool: ToolUseInfo) => void
도구 실행 시 콜백
onNotification
(data: NotificationData) => void
알림 (검색 결과, 지식 표시 등) 수신 시 콜백
onProposal
(proposal: ProposalData) => void
지식 변경 제안 시 콜백
onSessionChange
(sessionId: string) => void
세션 변경 시 콜백
반환값
프로퍼티 타입 설명 messagesChatMessage[]메시지 배열 (리치 메타데이터 포함) sessionsChatSession[]세션 목록 currentSessionIdstring | null현재 세션 ID isStreamingboolean스트리밍 중 여부 isLoadingSessionsboolean세션 로딩 중 여부 isLoadingMessagesboolean메시지 로딩 중 여부 toolUseToolUseInfo | null실행 중인 도구 정보 pendingProposalProposalData | null대기 중인 지식 변경 제안
메서드 설명 sendMessage(message)메시지 전송 loadSessions()세션 목록 로드 loadSession(id)세션 로드 startNewSession()새 세션 시작 deleteSession(id)세션 삭제 approveProposal()제안 승인 rejectProposal()제안 거부 editProposal(content)제안 수정 후 승인
타입 익스포트
@neuradex/sdk/react에서 다음 타입이 모두 익스포트됩니다.
import type {
// Hook
UseChatOptions ,
UseChatReturn ,
ChatFeature ,
// 메시지
ChatSession ,
ChatMessage ,
RichMessageMetadata ,
// 도구 & 제안
ToolUseInfo ,
ProposalData ,
NotificationData ,
KnowledgeActionType ,
UserResponseType ,
NotificationType ,
// 리치 UI 페이로드
KnowledgeProposalPayload ,
KnowledgeResultPayload ,
KnowledgeViewPayload ,
SearchResultPayload ,
EpisodeQueryResultPayload ,
EpisodeListResultPayload ,
// 아이템 타입
SearchResultItem ,
ConnectedKnowledgeItem ,
EpisodeResultItem ,
} from '@neuradex/sdk/react' ;
다음 단계
Chat API 서버 사이드 Chat Completions