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}
)}
)}
);
}