메인 콘텐츠로 건너뛰기
@neuradex/sdk/reactLibrarian / 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의 메시지는 일반 텍스트뿐만 아니라, rolemetadata.type 조합으로 다양한 UI 요소를 표현합니다.

메시지 역할

role설명
user사용자 입력
assistantAI 응답 (텍스트, Markdown)
notification검색 결과, 지식 상세 등의 리치 UI
interactive_action사용자 조작이 필요한 제안 UI
tool도구 실행 정보

메타데이터 타입

notificationinteractive_action 역할의 메시지는 metadata 필드에 리치 UI 정보를 가집니다.
metadata.type설명UI 예시
search_result시맨틱 검색 결과검색 쿼리, 히트 수, 결과 목록
knowledge_view지식 상세 표시제목, 콘텐츠, 태그, 관련 지식
knowledge_result지식 작업 완료 알림추가/수정/삭제 성공 표시
knowledge_proposal지식 변경 제안승인·거부·수정 인터랙티브 UI
episode_query_result에피소드 검색 결과검색 쿼리, 시간 범위, 결과 목록
episode_list_result에피소드 목록요약, 질문/답변 수, 결과 목록

메시지 렌더링

message.rolemessage.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는 범용적인 nameargs를 제공하므로, 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'
필수
기능 타입
projectId
string
프로젝트 ID (Librarian용)
chatbotId
string
Chatbot ID (Chatbot용)
endpoint
string
필수
API 엔드포인트 URL
getAuthHeaders
() => Promise<Record<string, string>>
인증 헤더를 반환하는 함수
headers
Record<string, string>
추가 헤더
onToolUse
(tool: ToolUseInfo) => void
도구 실행 시 콜백
onNotification
(data: NotificationData) => void
알림 (검색 결과, 지식 표시 등) 수신 시 콜백
onProposal
(proposal: ProposalData) => void
지식 변경 제안 시 콜백
onSessionChange
(sessionId: string) => void
세션 변경 시 콜백
onError
(error: Error) => 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

Knowledge API

지식 관리