`backend` 옆에 `front` 가 있을 이유가 없었다. negosium 의 negodata/front 를 그대로 베꼈고 그게 왜 front 인지는 따져보지 않았다 — 근거 없이 들여온 이름이라 바로잡는다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
146 lines
4.6 KiB
TypeScript
146 lines
4.6 KiB
TypeScript
import React from 'react';
|
|
|
|
export interface SlateTextNode {
|
|
text: string;
|
|
bold?: boolean;
|
|
italic?: boolean;
|
|
underline?: boolean;
|
|
code?: boolean;
|
|
}
|
|
|
|
export interface SlateElementNode {
|
|
type?: string;
|
|
speaker?: string;
|
|
role?: 'buyer' | 'seller' | 'host' | 'guest' | 'ai' | 'customer';
|
|
time?: string;
|
|
children: (SlateElementNode | SlateTextNode)[];
|
|
}
|
|
|
|
interface SlateScriptRendererProps {
|
|
nodes?: (SlateElementNode | SlateTextNode)[];
|
|
primaryColor?: string;
|
|
className?: string;
|
|
}
|
|
|
|
export const SlateScriptRenderer: React.FC<SlateScriptRendererProps> = ({
|
|
nodes,
|
|
primaryColor = '#18181b',
|
|
className = '',
|
|
}) => {
|
|
if (!nodes || nodes.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const renderTextNode = (node: SlateTextNode, idx: number) => {
|
|
let content: React.ReactNode = node.text;
|
|
if (node.bold) content = <strong key={idx}>{content}</strong>;
|
|
if (node.italic) content = <em key={idx}>{content}</em>;
|
|
if (node.underline) content = <u key={idx}>{content}</u>;
|
|
if (node.code) {
|
|
content = (
|
|
<code key={idx} className="bg-slate-100 px-1 py-0.5 rounded text-[11px] font-mono">
|
|
{content}
|
|
</code>
|
|
);
|
|
}
|
|
return <React.Fragment key={idx}>{content}</React.Fragment>;
|
|
};
|
|
|
|
const renderElement = (node: SlateElementNode, idx: number) => {
|
|
switch (node.type) {
|
|
case 'chat-dialog':
|
|
case 'dialog':
|
|
case 'negotiation': {
|
|
const isHost = node.role === 'seller' || node.role === 'host' || node.role === 'ai';
|
|
return (
|
|
<div
|
|
key={idx}
|
|
className={`flex items-start gap-3 my-2.5 ${isHost ? 'flex-row-reverse' : 'flex-row'}`}
|
|
>
|
|
{/* Avatar */}
|
|
<div
|
|
className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0 shadow-2xs ${
|
|
isHost ? 'bg-slate-900 text-white' : 'bg-slate-200 text-slate-700'
|
|
}`}
|
|
>
|
|
{node.speaker ? node.speaker.slice(0, 1) : isHost ? '호' : '손'}
|
|
</div>
|
|
|
|
{/* Bubble */}
|
|
<div className={`max-w-[75%] space-y-1 ${isHost ? 'text-right' : 'text-left'}`}>
|
|
<div className="flex items-center gap-1.5 text-[11px] text-slate-400">
|
|
<span className="font-semibold text-slate-700">{node.speaker || (isHost ? '호스트' : '게스트')}</span>
|
|
{node.time && <span>· {node.time}</span>}
|
|
</div>
|
|
<div
|
|
className={`px-3.5 py-2.5 rounded-2xl text-xs leading-relaxed inline-block text-left ${
|
|
isHost
|
|
? 'bg-slate-900 text-white rounded-tr-xs shadow-2xs'
|
|
: 'bg-slate-100 text-slate-800 rounded-tl-xs'
|
|
}`}
|
|
>
|
|
{node.children.map((child, cIdx) =>
|
|
'text' in child ? renderTextNode(child, cIdx) : renderElement(child, cIdx)
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
case 'heading-two':
|
|
case 'h2':
|
|
return (
|
|
<h3 key={idx} className="text-base font-bold text-slate-900 mt-4 mb-2">
|
|
{node.children.map((child, cIdx) =>
|
|
'text' in child ? renderTextNode(child, cIdx) : renderElement(child, cIdx)
|
|
)}
|
|
</h3>
|
|
);
|
|
|
|
case 'quote':
|
|
case 'blockquote':
|
|
return (
|
|
<blockquote
|
|
key={idx}
|
|
className="border-l-2 pl-3 my-3 text-xs italic text-slate-600 border-slate-400"
|
|
>
|
|
{node.children.map((child, cIdx) =>
|
|
'text' in child ? renderTextNode(child, cIdx) : renderElement(child, cIdx)
|
|
)}
|
|
</blockquote>
|
|
);
|
|
|
|
case 'callout':
|
|
case 'info-box':
|
|
return (
|
|
<div
|
|
key={idx}
|
|
className="p-3.5 my-3 bg-slate-50 border border-slate-200 rounded-xl text-xs text-slate-700 leading-relaxed"
|
|
>
|
|
{node.children.map((child, cIdx) =>
|
|
'text' in child ? renderTextNode(child, cIdx) : renderElement(child, cIdx)
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
default:
|
|
return (
|
|
<p key={idx} className="text-xs text-slate-600 leading-relaxed my-1.5">
|
|
{node.children.map((child, cIdx) =>
|
|
'text' in child ? renderTextNode(child, cIdx) : renderElement(child, cIdx)
|
|
)}
|
|
</p>
|
|
);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={`slate-editor-script space-y-1 ${className}`}>
|
|
{nodes.map((node, idx) =>
|
|
'text' in node ? renderTextNode(node, idx) : renderElement(node, idx)
|
|
)}
|
|
</div>
|
|
);
|
|
};
|