import { Sparkles } from 'lucide-react'; import type { SessionData } from '@/api/generated/model/sessionData'; import type { QuotationCardData } from '@/api/generated/model/quotationCardData'; import type { ChatMessageData } from '@/api/generated/model/chatMessageData'; import { ChatSender, CardType } from '@/api/generated/model'; import SlateRenderer from '@/components/SlateRenderer'; import { Typography } from '@/components/ui/typography'; import { StatusPill, sessionStatusTone } from './StatusPill'; import { type Product, type Partner, sessionStatusLabel } from '../../types'; import { maskPrices } from '@/lib/utils'; import { renderEmphasis } from '@/lib/emphasis'; export function ChatTab({ serverSessions, partners, effectiveSessionId, onSelectSession, chatMessages, currentSupplierName, currentProduct, serverCards, }: { serverSessions: SessionData[]; partners: Partner[]; effectiveSessionId: string | null; onSelectSession: (sessionId: string) => void; chatMessages: ChatMessageData[]; currentSupplierName: string; currentProduct: Product | undefined; serverCards: QuotationCardData[]; }) { // 목표가는 협상(세션) 단위 고정값(sessions.target_price)이라 메시지마다가 아니라 헤더에 한 번만 표시한다. const currentSession = serverSessions.find((s) => s.session_id === effectiveSessionId); const targetPrice = currentSession?.target_price; return (
{/* Sessions list */}
참여자 협력사 리스트
{serverSessions.length === 0 && ( 참여 협상 세션이 없습니다. (리스트가 비어 있습니다) )} {serverSessions.map((sd) => { const isSelected = sd.session_id === effectiveSessionId; const name = partners.find((p) => p.id === sd.supplier_id)?.name || sd.supplier_id; const statusLabel = sessionStatusLabel(sd.status); return ( ); })}
{/* Chat zone */}
협력사: {currentSupplierName}
{targetPrice != null && ( 목표가: ₩{Number(targetPrice).toLocaleString()} )} 기록: {chatMessages.length} 메시지
{!effectiveSessionId ? ( 선택된 협력사가 없습니다. ) : chatMessages.length === 0 ? ( 기록된 협상 대화가 없습니다. ) : ( chatMessages.map((m) => m.sender === ChatSender.BOT ? ( ) : ( ), ) )}
); } // 봇(좌측) 말풍선. 진행 단계·협상 스크립트(가격 마스킹)·사용 협상카드를 보여준다. // 간격은 부모(flex flex-col gap)에서 주고, 말풍선 박스는 block 으로 둬 긴 텍스트 줄바꿈이 깨지지 않게 한다. function BotBubble({ message, currentSupplierName, currentProduct, serverCards, }: { message: ChatMessageData; currentSupplierName: string; currentProduct: Product | undefined; serverCards: QuotationCardData[]; }) { const m = message; return (
Negosium Bot · #{m.index}
{m.step && ( {m.step} )} {m.script && ( {renderEmphasis(maskPrices(m.script))} )}
); } // 협력사(우측) 말풍선. 협력사 입력값을 보여주되, 채팅 '내용'에 제시 금액이 노출되지 않도록 maskPrices 로 가린다. function PartnerBubble({ message, currentSupplierName, currentProduct, serverCards, }: { message: ChatMessageData; currentSupplierName: string; currentProduct: Product | undefined; serverCards: QuotationCardData[]; }) { const m = message; return (
{currentSupplierName} · #{m.index}
{m.step && ( {m.step} )} {m.script && ( {maskPrices(m.script)} )}
); } // 말풍선에 붙는 협상카드 박스(봇/협력사 공용). 카드 미사용 메시지면 아무것도 렌더하지 않는다. // 톤(amber/primary)만 isBot 으로 가르고, 멘트/조건/메모 렌더 로직은 공유한다. function UsedCardBox({ message, isBot, currentSupplierName, currentProduct, serverCards, }: { message: ChatMessageData; isBot: boolean; currentSupplierName: string; currentProduct: Product | undefined; serverCards: QuotationCardData[]; }) { const m = message; // 메시지가 쓴 협상카드 전체(이름만이 아니라 멘트/조건/메모까지) 를 chat_id 로 매칭. const usedCard = m.card_used_yn ? serverCards.find((c) => c.session_card_id === m.chat_id) : undefined; if (!usedCard) return null; const cardNodes = Array.isArray(usedCard.edit_script) ? (usedCard.edit_script as unknown[]) : null; const isWildCard = usedCard.type === CardType.WILD; return (
{/* 헤더: 어떤 카드인지(번호·이름·종류) */}
협상카드 {usedCard.number && ( #{usedCard.number} )} {usedCard.name && ( · {usedCard.name} )} {isWildCard ? '와일드' : '협상'}
{/* 멘트 본문: 서식본(edit_script) 우선, 없으면 평문 script */} {cardNodes ? (
) : usedCard.script ? ( {usedCard.script} ) : null} {/* 와일드카드 부가 정보: 사용 조건 / 메모 */} {isWildCard && (usedCard.condition || usedCard.memo) && (
{usedCard.condition && ( 조건: {usedCard.condition} )} {usedCard.memo && ( 메모: {usedCard.memo} )}
)}
); }