Files
AzerothAI/app/realm-insights.tsx
T
2026-09-22 21:18:32 -04:00

24 lines
5.5 KiB
TypeScript

'use client';
import {useEffect,useState} from 'react';
import {Input} from '@/components/ui/input';
import type {summarize} from '@/core/realm-insights';
type Report=ReturnType<typeof summarize>;
type Activity={name:string;zone:string;action:string;label:string;detail:string;observedAt:number|null;lastProgressAt:number|null;recoveries:{at:number;detail:string}[]};
type Insights={collectedAt:number|null;startedAt:number|null;nextReportAt:number;current:Report;reports:Report[];activity:Activity[]};
const stamp=(at:number|null)=>at?new Date(at*1000).toLocaleString():'Not yet observed';
const metrics:Record<string,string>={levels:'Levels gained',deaths:'Verified deaths',arrivals:'First arrivals',conversations:'Bot exchanges',bonds:'New positive bonds',parties:'Party invitations',recruits:'Guild admissions',guild:'Guild goal progress',recoveries:'Recovery attempts'};
export function RealmInsights({mode,initialQuery=''}:{mode:string;initialQuery?:string}){
const [data,setData]=useState<Insights|null>(null),[error,setError]=useState(''),[selected,setSelected]=useState('current'),[query,setQuery]=useState(initialQuery),[attention,setAttention]=useState(false);
useEffect(()=>{let stopped=false;const poll=async()=>{try{const r=await fetch('/api/realm-insights');if(!r.ok)throw new Error('Insights temporarily unavailable');const d=await r.json() as Insights;if(!stopped){setData(d);setError('');}}catch(e){if(!stopped)setError(e instanceof Error?e.message:'Unable to load insights');}};void poll();const timer=setInterval(poll,15000);return()=>{stopped=true;clearInterval(timer);};},[]);
if(!data)return <div className="empty-note" role="status">{error||'Loading realm observations…'}</div>;
const report=selected==='current'?data.current:data.reports.find(r=>String(r.start)===selected)??data.current;
const rows=data.activity.filter(b=>`${b.name} ${b.zone} ${b.label}`.toLowerCase().includes(query.toLowerCase())&&(!attention||['Possible stall','Telemetry delayed'].includes(b.label)));
return <><p className="fineprint">Last collection: {stamp(data.collectedAt)} · Refreshes every 15 seconds.</p>{error&&<p role="alert" className="notice">{error}; showing the last available observations.</p>}{(!data.collectedAt||Date.now()/1000-data.collectedAt>180)&&<p role="status" className="notice">Collection is pending or delayed. Report totals may be incomplete.</p>}
{mode==='Nightly Report'?<>
<section className="panel"><div className="panel-body"><label className="field">Report period<select value={selected} onChange={e=>setSelected(e.target.value)}><option value="current">Today · in progress</option>{data.reports.map(r=><option key={r.start} value={r.start}>{stamp(r.start)} — {stamp(r.end)}</option>)}</select></label><p>{stamp(report.start)} — {selected==='current'?'now':stamp(report.end)}</p><p className="fineprint">Reports close daily at 06:00 UTC. Next close: {stamp(data.nextReportAt)}. Reports are kept for 30 days.</p><p className="fineprint">{report.partial?'Partial period · ':''}Observations since {stamp(report.observedSince)} · {report.samples} minute samples. Gaps can miss activity; these are observed changes, not a complete game log.</p></div></section>
<div className="stats" style={{marginTop:20}}>{Object.entries(metrics).map(([key,label])=><div className="panel stat" key={key}><div className="stat-label">{label}</div><div className="stat-value">{report.totals[key]??0}</div></div>)}</div>
<section className="panel"><div className="panel-head"><h2>Realm highlights</h2></div><div className="panel-body">{report.highlights.length?report.highlights.map(e=><div className="memory" key={e.id}><strong>{e.name}</strong><p>{e.detail}</p><small>{stamp(e.at)}</small></div>):<p className="empty-note">No changes recorded in this period yet.</p>}<p className="fineprint">Exchanges count confirmed bot replies, excluding synthetic openers and private player messages. Positive bonds need not be mutual friendships. Arrivals mean first observed online, not necessarily newly created. Recovery attempts do not prove recovery succeeded.</p></div></section>
</>:<><section className="panel"><div className="panel-body"><Input aria-label="Search bot activity" placeholder="Search by name, zone, or activity" value={query} onChange={e=>setQuery(e.target.value)}/><label style={{display:'flex',gap:8,marginTop:16}}><input type="checkbox" checked={attention} onChange={e=>setAttention(e.target.checked)}/>Show possible stalls and delayed observations</label><p className="fineprint">{rows.length} of {data.activity.length} online residents. Progress means observed movement, level gain, or health change. Intentions describe plans, not completed game actions.</p></div></section><div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit,minmax(min(100%,320px),1fr))',gap:16,marginTop:20}}>{rows.map(b=><section className="panel" key={b.name}><div className="panel-head"><h2>{b.name}</h2><span className="tag">{b.label}</span></div><div className="panel-body"><p>{b.zone} · Intent: {b.action}</p><p style={{marginTop:12}}>{b.detail}</p><p className="fineprint">Game observation: {stamp(b.observedAt)}<br/>Last observed progress: {stamp(b.lastProgressAt)}</p><details><summary>Recent recovery attempts ({b.recoveries.length})</summary>{b.recoveries.length?b.recoveries.map((r,i)=><p className="fineprint" key={i}>{stamp(r.at)} · {r.detail}</p>):<p className="fineprint">No retained attempts. Recovery requires ten minutes without native progress, safe conditions, and an expired 30-minute cooldown.</p>}</details></div></section>)}</div>{!rows.length&&<p className="empty-note">No residents match this filter.</p>}</>}
</>;
}