feat(web): implement AutopilotView (Wave B) on cocotte ui + getAutopilot -- web/src/views/AutopilotView.tsx
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9ad22c5e6d
commit
185253f044
1 changed files with 127 additions and 0 deletions
127
web/src/views/AutopilotView.tsx
Normal file
127
web/src/views/AutopilotView.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { usePoll, formatTime } from '../usePoll';
|
||||
import { getAutopilot, type AutopilotItem } from '../api';
|
||||
import { navigate } from '../useHashRoute';
|
||||
import { Button, Card, ErrText, HStack, Muted, Pill, StatusDot, Title, VStack } from '../ui';
|
||||
|
||||
/** Live per-thread autopilot states (Wave B unit).
|
||||
* Title + scrollable list of live states backed by GET /autopilot (runner/scheduler/tasks).
|
||||
* Each row: StatusDot + clickable handle (navigates to prospect detail), state badge (Pill colored per drafting/deciding/sending/sent/paused),
|
||||
* eta or at, campaign (warning Pill), leaf (mono), why, open action (ghost Button).
|
||||
* Renders using *only* cocotte ui/ primitives (no legacy classes, no shared file edits).
|
||||
* Real data; 4s poll for live feel. Matches prototype AUTOPILOT live activity (buildAutopilot) shape + tasks for states.
|
||||
*/
|
||||
export function AutopilotView(): JSX.Element {
|
||||
const { data, error, loading } = usePoll<{ items: AutopilotItem[] }>(() => getAutopilot(20), 4000);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<VStack $gap={8}>
|
||||
<Title>Autopilot</Title>
|
||||
<ErrText>{error}</ErrText>
|
||||
</VStack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading && !data) {
|
||||
return (
|
||||
<Card>
|
||||
<VStack $gap={8}>
|
||||
<Title>Autopilot</Title>
|
||||
<Muted>Loading live states…</Muted>
|
||||
</VStack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const items = data?.items ?? [];
|
||||
|
||||
return (
|
||||
<VStack $gap={12}>
|
||||
<VStack $gap={2}>
|
||||
<Title>Autopilot</Title>
|
||||
<Muted>what the AI is doing · live</Muted>
|
||||
</VStack>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<Muted>No live autopilot activity.</Muted>
|
||||
) : (
|
||||
<VStack $gap={8}>
|
||||
{items.map((item) => (
|
||||
<AutopilotRow key={item.pid} item={item} />
|
||||
))}
|
||||
</VStack>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
function AutopilotRow({ item }: { item: AutopilotItem }): JSX.Element {
|
||||
const open = () => navigate('prospect', item.handle);
|
||||
const tone = getToneForState(item.state);
|
||||
const dotStatus = getDotForState(item.state);
|
||||
const timeLabel = getEtaOrAt(item);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<VStack $gap={6}>
|
||||
<HStack $gap={8} $wrap $align="center">
|
||||
<StatusDot $status={dotStatus} />
|
||||
<Button
|
||||
$variant="ghost"
|
||||
onClick={open}
|
||||
style={{ padding: 0, fontSize: '13px', fontWeight: 600 }}
|
||||
>
|
||||
{item.handle}
|
||||
</Button>
|
||||
<Pill $tone={tone}>{item.state}</Pill>
|
||||
{timeLabel ? <Muted style={{ fontSize: '10.5px' }}>{timeLabel}</Muted> : null}
|
||||
<div style={{ flex: 1 }} />
|
||||
<Pill $tone="warning">{item.campaign}</Pill>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '10.5px',
|
||||
fontFamily: 'ui-monospace, Menlo, monospace',
|
||||
color: '#34d399',
|
||||
}}
|
||||
>
|
||||
{item.leaf}
|
||||
</span>
|
||||
</HStack>
|
||||
<Muted style={{ fontStyle: 'italic', fontSize: '11.5px' }}>why → {item.why}</Muted>
|
||||
<HStack $justify="flex-end" $gap={8}>
|
||||
<Button $variant="ghost" onClick={open} style={{ fontSize: '11.5px', padding: '3px 8px' }}>
|
||||
Open
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function getToneForState(state: string): 'accent' | 'neutral' | 'success' | 'warning' | 'error' {
|
||||
const s = (state || '').toLowerCase();
|
||||
if (s === 'sending') return 'warning';
|
||||
if (s === 'sent') return 'success';
|
||||
if (s === 'paused') return 'neutral';
|
||||
if (s === 'deciding' || s === 'drafting') return 'accent';
|
||||
return 'neutral';
|
||||
}
|
||||
|
||||
function getDotForState(state: string): 'up' | 'degraded' | 'down' {
|
||||
const s = (state || '').toLowerCase();
|
||||
if (s === 'sent') return 'up';
|
||||
if (s === 'paused') return 'down';
|
||||
return 'degraded';
|
||||
}
|
||||
|
||||
function getEtaOrAt(item: AutopilotItem): string {
|
||||
if (item.eta != null) {
|
||||
return `~${item.eta}s`;
|
||||
}
|
||||
if (item.at) {
|
||||
return formatTime(item.at);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue