// Sidebar.jsx — left rail, header, popmenus
function PopMenu({ items, onClose, style }) {
const ref = React.useRef(null);
React.useEffect(() => {
const onClick = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
const tid = setTimeout(() => document.addEventListener('mousedown', onClick), 0);
document.addEventListener('keydown', onKey);
return () => { clearTimeout(tid); document.removeEventListener('mousedown', onClick); document.removeEventListener('keydown', onKey); };
}, []);
return (
e.stopPropagation()}>
{items.map((it, i) => it.divider
?
: it.section
?
{it.section}
:
)}
);
}
function ChatRail({ threads, active, onSelect, onNew, onOpenSettings, onOpenAdmin, onSearch, onThreadAction, onSignOut, onClose, user }) {
const [menu, setMenu] = React.useState(null); // thread id whose ⋯ menu is open
const displayName = (user && user.display_name) || (user ? user.email : 'guest');
const subline = user
? ((user.is_admin ? 'admin' : 'member') + ' · ' + user.email)
: 'not signed in';
const initial = (displayName || '?')[0].toUpperCase();
return (
);
}
function ThreadRow({ t, active, menuOpen, onSelect, onMenu, onCloseMenu, onAction }) {
return (
{t.title}
{t.model} · {t.time}{t.pinned && ●}
{menuOpen &&
onAction('rename') },
{ ic: t.pinned ? '◌' : '★', label: t.pinned ? '고정 해제' : '맨 위로 고정', onClick: () => onAction('pin') },
{ ic: '↗', label: '프로젝트로 이동…', onClick: () => onAction('move') },
{ ic: '⎘', label: '복제', kbd: window.MOD_KEY + 'D', onClick: () => onAction('duplicate') },
{ divider: true },
{ ic: '⤴', label: '공유…', onClick: () => onAction('share') },
{ ic: '↓', label: '내보내기…', onClick: () => onAction('export') },
{ ic: '◫', label: '대화 ID 복사', onClick: () => onAction('copy-id') },
{ divider: true },
{ ic: '▢', label: '보관', onClick: () => onAction('archive') },
{ ic: '🗑', label: '대화 삭제', onClick: () => onAction('delete'), danger: true },
]}/>}
);
}
function ChatHeader({ title, model, temp, status, onMenu, onShare, onExport, onMoreAction }) {
const [moreOpen, setMoreOpen] = React.useState(false);
const dotColor = status === 'streaming' ? 'var(--accent)'
: status === 'ready' ? 'var(--zg-success)'
: status === 'error' ? 'var(--zg-danger)'
: 'var(--fg-5)';
const tempText = (typeof temp === 'number' ? temp : 0.2).toFixed(2);
return (
{onMenu && (
)}
{title}
{model}
temp {tempText}
{moreOpen &&
setMoreOpen(false)} style={{ position: 'absolute', right: 0, top: 36 }} items={[
{ ic: '✎', label: '대화 이름 변경', kbd: 'F2', onClick: () => onMoreAction && onMoreAction('rename') },
{ ic: '★', label: '맨 위로 고정', onClick: () => onMoreAction && onMoreAction('pin') },
{ ic: '↗', label: '프로젝트로 이동…', onClick: () => onMoreAction && onMoreAction('move') },
{ ic: '⎘', label: '복제', kbd: window.MOD_KEY + 'D', onClick: () => onMoreAction && onMoreAction('duplicate') },
{ divider: true },
{ section: '검사' },
{ ic: '⚙', label: '플레이그라운드에서 열기', kbd: window.MOD_KEY + '.', onClick: () => onMoreAction && onMoreAction('playground') },
{ ic: '◫', label: '대화 ID 복사', onClick: () => onMoreAction && onMoreAction('copy-id') },
{ ic: '⛨', label: '인증서 보기', onClick: () => onMoreAction && onMoreAction('cert') },
{ divider: true },
{ ic: '▢', label: '보관', onClick: () => onMoreAction && onMoreAction('archive') },
{ ic: '🗑', label: '대화 삭제', onClick: () => onMoreAction && onMoreAction('delete'), danger: true },
]}/>}
);
}
window.ChatRail = ChatRail;
window.ChatHeader = ChatHeader;
window.PopMenu = PopMenu;