// 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;