← back to Omega Watches 2
src/components/panels/WilliamPanel.tsx
154 lines
'use client';
import { useState, useEffect, useRef } from 'react';
interface WilliamMessage {
timestamp: string;
text: string;
type: 'observation' | 'alert' | 'commentary' | 'action';
}
interface WilliamStatus {
status: string;
agent: string;
paused: boolean;
processing: boolean;
color: string;
uptime: number;
}
const WILLIAM_URL = '/api/william';
function formatUptime(seconds: number): string {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
if (h > 0) return `${h}h ${m}m`;
return `${m}m`;
}
function formatTime(ts: string): string {
try {
return new Date(ts).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
} catch {
return '';
}
}
export default function WilliamPanel() {
const [messages, setMessages] = useState<WilliamMessage[]>([]);
const [status, setStatus] = useState<WilliamStatus | null>(null);
const [loading, setLoading] = useState(true);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
fetchData();
const interval = setInterval(fetchData, 30000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages]);
async function fetchData() {
try {
const [obsRes, statusRes] = await Promise.all([
fetch(WILLIAM_URL),
fetch(`${WILLIAM_URL}?action=status`),
]);
if (obsRes.ok) {
const obsData = await obsRes.json();
setMessages(obsData.observations || []);
}
if (statusRes.ok) {
const statusData = await statusRes.json();
setStatus(statusData);
}
} catch {
// Silently handle — William may be offline
} finally {
setLoading(false);
}
}
const typeStyles: Record<string, { bg: string; border: string; icon: string }> = {
alert: { bg: 'bg-red-900/20', border: 'border-red-800/40', icon: '!' },
observation: { bg: 'bg-[#722F37]/10', border: 'border-[#722F37]/30', icon: 'W' },
commentary: { bg: 'bg-[#722F37]/5', border: 'border-[#722F37]/20', icon: '~' },
action: { bg: 'bg-amber-900/20', border: 'border-amber-800/30', icon: '>' },
};
return (
<div className="flex h-full flex-col rounded-xl border border-[#722F37]/30 bg-[#722F37]/5">
{/* Header */}
<div className="flex items-center justify-between border-b border-[#722F37]/20 px-3 py-2.5"
style={{ background: 'linear-gradient(135deg, #722F37 0%, #4a1f24 100%)' }}>
<div className="flex items-center gap-2">
<span className="text-lg">🎩</span>
<div>
<div className="text-sm font-bold text-white">William</div>
<div className="text-[10px] text-gray-300">Autonomous Watch Agent</div>
</div>
</div>
<div className="flex items-center gap-2">
{status && (
<>
<span className={`h-2 w-2 rounded-full ${status.paused ? 'bg-amber-400' : 'bg-green-400'} animate-pulse`} />
<span className="text-[10px] text-gray-300">
{status.paused ? 'Paused' : 'Active'} · {formatUptime(status.uptime)}
</span>
</>
)}
</div>
</div>
{/* Messages feed */}
<div ref={scrollRef} className="flex-1 space-y-2 overflow-y-auto p-2.5">
{loading ? (
<div className="flex h-32 items-center justify-center">
<div className="text-xs text-gray-500">Connecting to William...</div>
</div>
) : messages.length === 0 ? (
<div className="flex h-32 flex-col items-center justify-center gap-2 text-center">
<span className="text-2xl">🎩</span>
<p className="text-xs text-gray-500">
William is observing the market.<br />
Commentary shall appear presently.
</p>
</div>
) : (
messages.map((msg, i) => {
const style = typeStyles[msg.type] || typeStyles.observation;
return (
<div
key={i}
className={`rounded-lg border ${style.border} ${style.bg} p-2.5`}
>
<div className="flex items-center gap-1.5">
<span className="flex h-4 w-4 items-center justify-center rounded bg-[#722F37] text-[9px] font-bold text-white">
{style.icon}
</span>
<span className="text-[10px] text-gray-500">{formatTime(msg.timestamp)}</span>
<span className="rounded bg-[#722F37]/30 px-1 text-[9px] text-[#C89B3C]">{msg.type}</span>
</div>
<p className="mt-1 text-xs leading-relaxed text-gray-300" style={{ fontFamily: 'Georgia, serif' }}>
{msg.text}
</p>
</div>
);
})
)}
</div>
{/* Footer */}
<div className="border-t border-[#722F37]/20 px-3 py-2 text-center">
<div className="text-[10px] text-gray-500" style={{ fontFamily: 'Georgia, serif' }}>
“One observes the market with the keenest attention.”
</div>
</div>
</div>
);
}