2026-06-03 05:08:00 +00:00

129 lines
5.3 KiB
JavaScript

// ChatPane — mirrors ui/src/components/chat/* with user/ask/result/progress bubbles
function Bubble({ role, children, footer }) {
const isUser = role === 'user';
const style = {
maxWidth: '85%',
padding: '10px 14px',
borderRadius: 16,
fontSize: 13,
lineHeight: 1.55,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
};
if (isUser) {
Object.assign(style, { background: '#2563eb', color: '#fff', borderBottomRightRadius: 4, alignSelf: 'flex-end' });
} else if (role === 'ask') {
Object.assign(style, { background: '#fef9c3', color: '#854d0e', border: '1px solid #fde68a', borderBottomLeftRadius: 4 });
} else if (role === 'result') {
Object.assign(style, { background: '#ecfdf5', color: '#065f46', border: '1px solid #a7f3d0', borderBottomLeftRadius: 4 });
} else {
Object.assign(style, { background: '#fff', color: '#0f172a', border: '1px solid #e2e8f0', borderBottomLeftRadius: 4 });
}
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: isUser ? 'flex-end' : 'flex-start', gap: 4 }}>
<div style={style}>{children}</div>
{footer && <div style={{ fontSize: 10, color: '#94a3b8' }}>{footer}</div>}
</div>
);
}
function ProgressBubble({ text }) {
return (
<div style={{
alignSelf: 'flex-start', background: '#f1f5f9', color: '#475569',
padding: '8px 12px', borderRadius: 12, fontSize: 12,
display: 'inline-flex', alignItems: 'center', gap: 8,
}}>
<Spinner />
<span>{text}</span>
</div>
);
}
function ChatHeader({ task, onOpenDetail, detailOpen }) {
return (
<div style={{
flexShrink: 0, padding: '12px 16px', borderBottom: '1px solid #e2e8f0', background: '#fff',
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
}}>
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: 10, fontFamily: 'IBM Plex Mono, monospace', color: '#94a3b8', letterSpacing: '.08em' }}>
TASK #{task.id}
</div>
<div style={{ fontSize: 15, fontWeight: 700, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{task.title}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<StatusBadge status={task.status} />
<button onClick={onOpenDetail} style={{
padding: '6px 10px', borderRadius: 8, border: '1px solid #e2e8f0',
background: detailOpen ? '#eff6ff' : '#fff',
color: detailOpen ? '#1d4ed8' : '#475569',
fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit',
}}>詳細</button>
</div>
</div>
);
}
function Composer({ onSend }) {
const [text, setText] = React.useState('');
const send = () => { if (!text.trim()) return; onSend(text.trim()); setText(''); };
return (
<div style={{ flexShrink: 0, borderTop: '1px solid #e2e8f0', background: '#fff', padding: 12 }}>
<div style={{
display: 'flex', alignItems: 'flex-end', gap: 8, background: '#f8fafc',
border: '1px solid #e2e8f0', borderRadius: 12, padding: 8,
}}>
<button style={{ padding: 6, background: 'transparent', border: 'none', color: '#94a3b8', cursor: 'pointer' }}>
<IconAttach width={16} height={16} />
</button>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); send(); } }}
rows={2}
placeholder="メッセージを入力 (⌘+Enter で送信)"
style={{
flex: 1, resize: 'none', border: 'none', outline: 'none', background: 'transparent',
fontFamily: 'inherit', fontSize: 13, color: '#0f172a', lineHeight: 1.5, minHeight: 32,
}}
/>
<button onClick={send} style={{
padding: '6px 14px', background: '#2563eb', color: '#fff', borderRadius: 8,
fontSize: 12, fontWeight: 700, border: 'none', cursor: text.trim() ? 'pointer' : 'not-allowed',
opacity: text.trim() ? 1 : 0.5, fontFamily: 'inherit',
}}>送信</button>
</div>
<div style={{ marginTop: 6, fontSize: 10, color: '#94a3b8', paddingLeft: 4 }}>エージェントは常に /brainstorm /plan /implement のパイプラインで動作します</div>
</div>
);
}
function ChatPane({ task, messages, onSend, onOpenDetail, detailOpen }) {
const scrollRef = React.useRef(null);
React.useEffect(() => {
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}, [messages.length, task.id]);
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', background: '#f8fafc' }}>
<ChatHeader task={task} onOpenDetail={onOpenDetail} detailOpen={detailOpen} />
<div ref={scrollRef} style={{
flex: 1, overflowY: 'auto', padding: '16px 20px',
display: 'flex', flexDirection: 'column', gap: 12, minHeight: 0,
}}>
{messages.map((m, i) => (
m.role === 'progress'
? <ProgressBubble key={i} text={m.content} />
: <Bubble key={i} role={m.role} footer={m.footer}>{m.content}</Bubble>
))}
</div>
<Composer onSend={onSend} />
</div>
);
}
window.ChatPane = ChatPane;