Open-source release of MAESTRO, an agent orchestration platform that runs LLM-driven tasks through sandboxed tools, with a web UI. Apache-2.0. See README.md and docs/ (getting-started, configuration, architecture).
93 lines
4.8 KiB
JavaScript
93 lines
4.8 KiB
JavaScript
// TaskList — FilterBar + LocalTaskListItem recreation
|
|
function FilterBar({ status, onStatus, search, onSearch, sort, onSort, counts, total }) {
|
|
const columns = ['queued', 'running', 'waiting_human', 'waiting_subtasks', 'retry', 'succeeded', 'failed', 'cancelled'];
|
|
const chipStyle = (active) => ({
|
|
flexShrink: 0, padding: '6px 10px', borderRadius: 9999,
|
|
fontSize: 11, fontWeight: 700, whiteSpace: 'nowrap', cursor: 'pointer',
|
|
border: '1px solid ' + (active ? '#2563eb' : '#e2e8f0'),
|
|
background: active ? '#eff6ff' : '#fff',
|
|
color: active ? '#1d4ed8' : '#64748b',
|
|
fontFamily: 'inherit',
|
|
});
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, paddingBottom: 12, borderBottom: '1px solid #e2e8f0' }}>
|
|
<div style={{
|
|
display: 'flex', alignItems: 'center', gap: 8, background: '#fff', border: '1px solid #e2e8f0',
|
|
borderRadius: 12, padding: '6px 12px', boxShadow: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
|
|
}}>
|
|
<IconSearch width={14} height={14} style={{ color: '#94a3b8', flexShrink: 0 }} />
|
|
<input value={search} onChange={(e) => onSearch(e.target.value)} placeholder="検索..."
|
|
style={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', fontSize: 13, fontFamily: 'inherit', color: '#0f172a', minWidth: 0 }} />
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 6, overflowX: 'auto', paddingBottom: 4 }}>
|
|
<button style={chipStyle(status === 'all')} onClick={() => onStatus('all')}>
|
|
All <span style={{ color: '#94a3b8', marginLeft: 2 }}>{total}</span>
|
|
</button>
|
|
{columns.map(s => (
|
|
<button key={s} style={chipStyle(status === s)} onClick={() => onStatus(s)}>
|
|
{STATUS_LABELS[s]} <span style={{ color: '#94a3b8', marginLeft: 2 }}>{counts[s] || 0}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
<select value={sort} onChange={(e) => onSort(e.target.value)} style={{
|
|
padding: '6px 10px', fontSize: 12, background: '#fff', border: '1px solid #e2e8f0',
|
|
borderRadius: 8, color: '#334155', outline: 'none', fontFamily: 'inherit',
|
|
}}>
|
|
<option value="updated">新しい順</option>
|
|
<option value="status">ステータス順</option>
|
|
<option value="title">タイトル順</option>
|
|
</select>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function TaskItem({ task, active, onClick }) {
|
|
return (
|
|
<button onClick={onClick} style={{
|
|
width: '100%', textAlign: 'left', padding: '10px 12px', borderRadius: 12,
|
|
border: '1px solid ' + (active ? '#3b82f6' : '#e2e8f0'),
|
|
background: active ? '#eff6ff' : '#fff',
|
|
cursor: 'pointer', transition: 'background .15s', fontFamily: 'inherit',
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, minWidth: 0 }}>
|
|
<div style={{ fontSize: 13, fontWeight: 700, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
#{task.id} {task.title}
|
|
</div>
|
|
<StatusBadge status={task.status} small />
|
|
</div>
|
|
<div style={{ marginTop: 2, fontSize: 11, color: '#64748b', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
{task.body.length > 60 ? task.body.slice(0, 60) + '…' : task.body}
|
|
</div>
|
|
<div style={{ marginTop: 2, fontSize: 10, color: '#94a3b8' }}>{relativeTime(task.updatedAt)}</div>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function TaskList({ tasks, activeId, onSelect, filters, setFilters }) {
|
|
const counts = {};
|
|
for (const s of ['queued', 'running', 'waiting_human', 'waiting_subtasks', 'retry', 'succeeded', 'failed', 'cancelled']) {
|
|
counts[s] = tasks.filter(t => t.status === s).length;
|
|
}
|
|
const filtered = tasks
|
|
.filter(t => filters.status === 'all' || t.status === filters.status)
|
|
.filter(t => !filters.search || (t.title + t.body).toLowerCase().includes(filters.search.toLowerCase()))
|
|
.sort((a, b) => filters.sort === 'title' ? a.title.localeCompare(b.title) : b.updatedAt - a.updatedAt);
|
|
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
|
<FilterBar
|
|
status={filters.status} onStatus={(s) => setFilters(f => ({ ...f, status: s }))}
|
|
search={filters.search} onSearch={(q) => setFilters(f => ({ ...f, search: q }))}
|
|
sort={filters.sort} onSort={(s) => setFilters(f => ({ ...f, sort: s }))}
|
|
counts={counts} total={tasks.length}
|
|
/>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 8, overflowY: 'auto', flex: 1, minHeight: 0 }}>
|
|
{filtered.map(t => <TaskItem key={t.id} task={t} active={activeId === t.id} onClick={() => onSelect(t.id)} />)}
|
|
{filtered.length === 0 && <div style={{ fontSize: 13, color: '#64748b', padding: '12px 8px' }}>スレッドがありません</div>}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
window.TaskList = TaskList;
|