AgentExecutor
먼저 보고, 그다음 계약을 읽습니다
Phase 8 AI-first 컴포지트 — 채팅 셸(AgentExecutor) + 점진 테이블(StreamingTable) + 멀티모달 입력(MultiModalChatInput).
Component intent의도와 경계 읽기
Phase 1-7 primitive(ChatInput / MessageBubble / StreamingMarkdown / ToolCallCard / Spinner / Button) 위에 얹는 high-level 조합 3종을 한 모듈로 export 한다. AgentExecutor 는 controlled 채팅 셸(messages 는 부모 보유, onSubmit 으로 새 사용자 메시지)이며 assistant 메시지는 StreamingMarkdown, 인라인 tool 은 ToolCallCard 로 렌더하고 sticky-bottom 자동 스크롤을 한다. StreamingTable 은 row 를 하나씩 append 하는 제너릭 테이블(진행 bar·pending spinner 행), MultiModalChatInput 은 ChatInput 을 첨부(drag/drop·paste·file picker)로 확장한 입력이다. 셋 다 barrel 최상위에서 직접 export 된다.
예제
SSOT에 등록된 실제 API 기준 snippet입니다. standalone 배지만 독립 실행 단위이며, fragment는 주변 state·handler 문맥을 생략합니다.
import { AgentExecutor, type ChatMessage } from "@axe/ui";const [messages, setMessages] = useState<ChatMessage[]>([]);<AgentExecutor messages={messages} streaming={pending} onCancel={abort} placeholder="무엇이든 물어보세요" onSubmit={(text) => send(text)}/>import { AgentExecutor, MultiModalChatInput, type ChatMessage } from "@axe/ui";const messages: ChatMessage[] = [ { id: "a1", role: "assistant", content: "장부를 조회했습니다.", tools: [{ id: "t1", name: "query_trial_balance", status: "success", output: "…" }], },];<AgentExecutor messages={messages} onSubmit={(text, files) => send(text, files)} inputSlot={ <MultiModalChatInput accept="image/*,application/pdf" maxFiles={3} onSubmit={(text, files) => send(text, files)} onReject={(reason, f) => toast(`${f.name}: ${reason}`)} /> }/>import { StreamingTable } from "@axe/ui";<StreamingTable columns={[ { key: "name", header: "종목" }, { key: "irr", header: "IRR", align: "right" }, ]} rows={rows} streaming={pending} expectedRowCount={20} getRowKey={(r) => r.id}/><div class="axe-streaming-table"> <div class="axe-streaming-table__scroll"> <table class="axe-streaming-table__table"> <thead class="axe-streaming-table__thead"> <tr><th class="axe-streaming-table__th">종목</th> <th class="axe-streaming-table__th axe-streaming-table__th--right">IRR</th></tr> </thead> <tbody> <tr class="axe-streaming-table__tr"> <td class="axe-streaming-table__td">A사</td> <td class="axe-streaming-table__td axe-streaming-table__td--right">18.4%</td> </tr> </tbody> </table> </div></div>Props
TSX 소스가 진실입니다. 주요 export 컴포넌트의 public API만 노출합니다.
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
messages | ChatMessage[] | 필수 | — | AgentExecutor: 렌더할 메시지 배열. ChatMessage = { id, role, content, tools?, attachments? }. |
onSubmit | (text: string, attachments?: File[]) => void | 필수 | — | AgentExecutor: 사용자 제출 콜백. attachments 는 inputSlot 으로 MultiModalChatInput 을 쓸 때만. |
onCancel | () => void | — | — | AgentExecutor: 주어지면 streaming=true 일 때 streaming-hint 에 '취소' 버튼 노출. |
streaming | boolean | — | false | AgentExecutor: 마지막 assistant 메시지 끝 cursor + 하단 '응답 생성 중…' hint. |
tools | ToolCallEvent[] | — | — | AgentExecutor: 메시지에 속하지 않는 free-floating tool 카드(보통 message.tools 권장). |
placeholder | string | — | — | AgentExecutor: 기본 ChatInput 의 placeholder(inputSlot 미사용 시). |
disabled | boolean | — | — | AgentExecutor: 기본 ChatInput 비활성(보통 streaming 중 true). |
inputRef | React.Ref<ChatInputHandle> | — | — | AgentExecutor: 기본 ChatInput 의 외부 ref(focus/clear). |
inputSlot | React.ReactNode | — | — | AgentExecutor: 입력 자리에 넣을 커스텀 노드 — 지정 시 기본 ChatInput 대체(예: MultiModalChatInput). |
ref → AgentExecutorHandle | { focus(): void; scrollToBottom(): void } | — | — | AgentExecutor: imperative 핸들 — 입력 focus·강제 맨아래 스크롤. |
StreamingTable.columns | StreamingTableColumn<R>[] | 필수 | — | StreamingTable: 열 정의. { key, header, render?, align?('left'|'right'|'center'), width? }. |
StreamingTable.rows | R[] | 필수 | — | StreamingTable: 현재까지의 행(추가되면 페이드인). |
StreamingTable.getRowKey | (row: R, idx: number) => string | 필수 | — | StreamingTable: 안정적 row key. |
StreamingTable.streaming | boolean | — | false | StreamingTable: 마지막에 spinner pending 행 노출(다음 행 대기). |
StreamingTable.expectedRowCount | number | — | — | StreamingTable: 총 행 수를 알면 상단 progress bar(0..100%). |
StreamingTable.emptyLabel | React.ReactNode | — | "—" | StreamingTable: 행 0 & 비스트리밍 시 placeholder. |
MultiModalChatInput.onSubmit | (text: string, attachments: File[]) => void | — | — | MultiModalChatInput: 텍스트+첨부 제출(ChatInput 과 다른 시그니처 — 그래서 별도 컴포넌트). |
MultiModalChatInput.accept | string | — | — | MultiModalChatInput: accept 패턴(예 'image/*,application/pdf'). '.pdf'/'image/*'/'application/pdf' 모두 매칭. |
MultiModalChatInput.maxFiles | number | — | 5 | MultiModalChatInput: 최대 첨부 개수(초과 시 onReject('count')). |
MultiModalChatInput.maxSizeBytes | number | — | 20 * 1024 * 1024 | MultiModalChatInput: 파일당 최대 크기(초과 시 onReject('size')). |
MultiModalChatInput.onReject | (reason: "count" | "size" | "type", file: File) => void | — | — | MultiModalChatInput: 거부 콜백(toast 등). |
MultiModalChatInput.toolbarLeft / toolbarRight / hint | React.ReactNode | — | — | MultiModalChatInput: 툴바 슬롯·힌트(hint 기본 '↵ 보내기 · ⇧↵ 줄바꿈 · 끌어 놓기 / 붙여넣기로 첨부'). |
MultiModalChatInput.ref → ChatInputHandle | { focus(): void; clear(): void } | — | — | MultiModalChatInput: imperative 핸들(clear 는 텍스트+첨부 모두 비움). |
.axe-* 클래스 계약
React 밖에서도 같은 표면을 그리는 공개 계약입니다. stable은 minor 버전 안에서 이름을 바꾸지 않습니다.
| 클래스 | 안정성 | 용도 |
|---|---|---|
.axe-agent-executor | stable | AgentExecutor 루트(세로 flex, 높이 100%, 테두리 카드). |
.axe-agent-executor__scroll | stable | 메시지 스크롤 영역(reduced-motion 시 smooth 해제). |
.axe-agent-executor__empty | stable | messages 0 일 때 '대화를 시작하세요' 빈 상태. |
.axe-agent-executor__user-content | stable | 사용자 메시지 본문(pre-wrap). |
.axe-agent-executor__tools | stable | 메시지 내부 ToolCallCard 목록 래퍼. --free 변형 = free-floating tools. |
.axe-agent-executor__attachments | stable | 사용자 메시지의 첨부 chip 행. |
.axe-agent-executor__streaming-hint | stable | 하단 '응답 생성 중…' 상태 바(Spinner + 선택 취소 버튼). |
.axe-agent-executor__input | stable | 입력 도크(상단 구분선, ChatInput 또는 inputSlot). |
.axe-chat-input--dragover | stable | MultiModal: drag-over 하이라이트(accent 테두리·ring). |
.axe-chat-input--disabled | stable | MultiModal: 비활성(opacity·pointer-events none). |
.axe-chat-input__drop-overlay | stable | MultiModal: '끌어 놓아 첨부' 드롭 오버레이(aria-hidden). |
.axe-attach-list | stable | 첨부 chip 목록(입력 위). |
.axe-attach-chip | stable | 첨부 chip. __thumb(이미지 썸네일)/__icon(비이미지)/__name/__size/__remove 파트. --compact = AgentExecutor 사용자 메시지용 mini. |
.axe-attach-button | stable | '📎 첨부' 파일 피커 버튼(maxFiles 도달 시 disabled). |
.axe-attach-file-input | internal | 숨긴 <input type=file>(display:none). |
.axe-streaming-table | stable | StreamingTable 루트(elevated 카드). data-streaming 속성. |
.axe-streaming-table__progress | stable | 상단 progress 트랙. __progress-bar 는 --axe-progress(0..1) scaleX 로 채움. |
.axe-streaming-table__scroll | stable | 가로 스크롤 래퍼(min-width 480px 테이블). |
.axe-streaming-table__table | stable | 테이블 본체(<table>). |
.axe-streaming-table__th | stable | 헤더 셀. --right/--center 정렬 변형. |
.axe-streaming-table__td | stable | 데이터 셀(tabular-nums). --right/--center 정렬 변형. |
.axe-streaming-table__tr--in | internal | 새 행 페이드인(axe-stream-row-in, reduced-motion 시 off). |
.axe-streaming-table__pending | stable | '다음 행 대기 중…' spinner 행(streaming 시). |
.axe-streaming-table__empty | stable | 행 0 & 비스트리밍 시 emptyLabel 셀. |
접근성
키보드, ARIA, 구현 노트를 함께 검토합니다.
- AgentExecutor: 기본 ChatInput 은 ↵ 보내기 · ⇧↵ 줄바꿈
- MultiModalChatInput: ↵ 보내기 · ⇧↵ 줄바꿈(IME 조합 중이면 submit 억제) · 붙여넣기/끌어 놓기로 첨부
- MultiModalChatInput: 각 chip 의 제거 버튼(aria-label "<파일명> 제거")로 삭제
AgentExecutor 빈 상태는 aria-live="polite", 하단 streaming-hint 는 role="status" aria-live="polite"(Spinner label="Streaming")로 진행을 고지한다. StreamingTable 은 시맨틱 <table> 이고 pending 행이 aria-live="polite" 로 '다음 행 대기 중' 을 알리지만, 상단 progress bar 는 aria-hidden 장식이라 수치 진행은 시각으로만 전달된다. MultiModalChatInput 의 파일 버튼은 aria-label="파일 첨부"(maxFiles 도달 시 disabled), 드롭 오버레이는 aria-hidden 이다.
AgentExecutor 의 자동 스크롤은 rAF 후 scrollTop 이동이며 사용자가 바닥에서 32px 이상 위로 스크롤하면 stick 해제된다(read 방해 없음). scroll-behavior 는 prefers-reduced-motion 에서 auto 로, StreamingTable 의 행 페이드인·progress transition 도 reduced-motion 에서 정지된다. MultiModal 의 dragenter/leave 는 중첩 카운팅으로 자식 위 호버 시 깜빡임을 막는다.