← 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' }}>
          &ldquo;One observes the market with the keenest attention.&rdquo;
        </div>
      </div>
    </div>
  );
}