reopt designreopt design
DocsExploreToolsPricingBuilder
Start
Overview
Start
Next.js 설치
Private install
Core Concepts
아키텍처
Composition Patterns
Accessibility
Keyboard Patterns
Styling
Theme System
Advanced Patterns
Build & Operate
Skills
AI Integration
CLI (opt surface add)
Dependency Graph
Tools
Canvas Catalog
Theme Builder
Form Builder
Templates
Templates
Releases
Release Notes
Oopt-ui
reopt designreopt design

A design system for the AI era

  • Docs
  • Pricing
  • Releases
  • GitHub
  • Terms of Service
  • Privacy Policy

© 2026 reopt-ai. All rights reserved.

ChatAssistant

surface

AI 챗봇 어시스턴트 화면. ChatSidebar + ChatMessageList + ChatInput 조합.

컴포넌트 의존 관계

깊이
▼ USES (4)ChatAssistantchat-sidebarchat-inputchat-message-listloading-overlay
100%

프로젝트 구조 설계

AI

새 프로젝트의 구조를 어떻게 잡으면 좋을까요?

오후 06:41
A

모노레포 구조를 추천합니다. Turborepo를 사용하면 빌드 캐싱과 태스크 오케스트레이션이 쉬워집니다. 기본 구조: - apps/ (애플리케이션) - packages/ (공유 패키지) - turbo.json (파이프라인)

오후 06:42
ShiftEnter로 줄바꿈

테스트 커버리지

2026년 2월 4일

생성된 테스트 결과를 찾지 못했습니다.

ChatAssistant 항목이 문서 메타에 연결되어 있지만 현재 생성 파일에는 없습니다.

테스트를 추가한 뒤 `bun run generate:test-results`를 실행하거나 `testDescribe` 매핑을 다시 확인하세요.

ChatAssistant Props

Prop타입기본값설명
conversationsChatConversationDef[]—사이드바 대화 목록
messagesChatMessageDef[]—현재 대화 메시지 배열
onSend(message: string) => void—메시지 전송 핸들러
modelsChatModelDef[]—모델 선택 옵션
stylePresetsChatStylePresetDef[]—스타일 프리셋 옵션
showSidebarbooleantrue사이드바 표시 여부
headerReactNode—상단 헤더 영역
actionsReactNode—헤더 우측 액션 버튼
loadingboolean—Surface 전체 로딩 상태
labelsChatAssistantLabels—i18n 라벨 오버라이드

Surface 설치

CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.

bash
npx @reopt-ai/opt-cli surface add chat-assistant

Consumer target

복사된 파일은 components/surfaces 아래에 저장됩니다.

tsx
import { ChatAssistant } from "@/components/surfaces/chat-assistant";

Registry metadata

설명
AI 챗봇 어시스턴트 화면. ChatSidebar + ChatMessageList + ChatInput 조합.
파일 수
1개
Registry dependencies
없음
Package dependencies
없음
태그
없음
Install notes
없음

포함 파일

  • chat-assistant.tsx→chat-assistant.tsx
Surface 소스 보기
chat-assistant.tsx
"use client";

import type { ReactNode } from "react";
import { cn, SurfaceLayout, ChatSidebar, ChatInput, ChatMessageList } from "@reopt-ai/opt-ui";
import type { ChatConversationDef, ChatMessageDef, ChatModelDef, ChatStylePresetDef, ChatSidebarLabels, ChatInputLabels, ChatMessageListLabels } from "@reopt-ai/opt-ui";

/** Localized labels for the chat assistant component. */
export interface ChatAssistantLabels {
  sidebar?: ChatSidebarLabels;
  input?: ChatInputLabels;
  messageList?: ChatMessageListLabels;
  emptyHistoryAnnouncement?: string;
}

/** Props for the chat assistant component. */
export interface ChatAssistantProps {
  conversations?: ChatConversationDef[];
  activeConversationId?: string;
  onConversationSelect?: (conversation: ChatConversationDef) => void;
  onNewChat?: () => void;
  messages?: ChatMessageDef[];
  onSend?: (message: string) => void;
  onFileAttach?: () => void;
  models?: ChatModelDef[];
  selectedModel?: string;
  onModelChange?: (modelId: string) => void;
  stylePresets?: ChatStylePresetDef[];
  selectedStyle?: string;
  onStyleChange?: (styleId: string) => void;
  assistantTyping?: boolean;
  assistantAvatar?: string;
  userName?: string;
  renderContent?: (message: ChatMessageDef) => ReactNode;
  showSidebar?: boolean;
  header?: ReactNode;
  actions?: ReactNode;
  loading?: boolean;
  labels?: ChatAssistantLabels;
  className?: string;
}

/** Renders the chat assistant component. */
export function ChatAssistant({
  conversations = [],
  activeConversationId,
  onConversationSelect,
  onNewChat,
  messages = [],
  onSend,
  onFileAttach,
  models,
  selectedModel,
  onModelChange,
  stylePresets,
  selectedStyle,
  onStyleChange,
  assistantTyping = false,
  assistantAvatar,
  userName,
  renderContent,
  showSidebar = true,
  header,
  actions,
  loading = false,
  labels,
  className,
}: ChatAssistantProps) {
  const isEmptyAssistant = conversations.length === 0 && messages.length === 0;

  return (
    <SurfaceLayout
      loading={loading}
      className={cn("h-full flex-row gap-0", className)}
      data-opt-id="AB2NY"
      data-opt-slug="chat-assistant.ChatAssistant"
    >
      {/* Sidebar */}
      {showSidebar && (
        <aside className="border-border flex w-[260px] shrink-0 flex-col border-r p-4">
          <ChatSidebar
            conversations={conversations}
            activeConversationId={activeConversationId}
            onConversationSelect={onConversationSelect}
            onNewChat={onNewChat}
            labels={labels?.sidebar}
          />
        </aside>
      )}

      {/* Main area */}
      <div className="flex min-w-0 flex-1 flex-col">
        {isEmptyAssistant && !loading && (
          <div className="sr-only" aria-live="polite">
            {labels?.emptyHistoryAnnouncement ?? "No chat history yet"}
          </div>
        )}
        {/* Header */}
        {(header || actions) && (
          <div className="border-border flex items-center justify-between border-b px-6 py-3">
            <div>{header}</div>
            {actions && (
              <div className="gap-element flex items-center">{actions}</div>
            )}
          </div>
        )}

        {/* Messages */}
        <ChatMessageList
          messages={messages}
          loading={assistantTyping}
          renderContent={renderContent}
          assistantAvatar={assistantAvatar}
          userName={userName}
          labels={labels?.messageList}
          className="flex-1"
        />

        {/* Input */}
        <div className="border-border shrink-0 border-t px-6 py-4">
          <ChatInput
            onSend={onSend}
            onFileAttach={onFileAttach}
            models={models}
            selectedModel={selectedModel}
            onModelChange={onModelChange}
            stylePresets={stylePresets}
            selectedStyle={selectedStyle}
            onStyleChange={onStyleChange}
            labels={labels?.input}
          />
        </div>
      </div>
    </SurfaceLayout>
  );
}

ChatAssistant.displayName = "ChatAssistant";