Add native gameplay outcomes, recovery verification, and realm diagnostics
Core tests / core (push) Successful in 40s

This commit is contained in:
Codex
2026-09-22 21:18:32 -04:00
parent 9d586f4c44
commit 290926f33f
18 changed files with 264 additions and 21 deletions
+7 -4
View File
@@ -1,8 +1,9 @@
'use client';
import {RealmDiagnostics} from './realm-diagnostics';
import {RealmInsights} from './realm-insights';
import {CommunityPanel,OperationsPanel} from './community-panel';
import { useState,useEffect,useCallback } from 'react';
import { LayoutDashboard,Users,BookOpen,Compass,Server,Activity,Brain,Sparkles,Plus,Play,Pause,ArrowRight,Shield,Terminal,ScrollText,Radio,RefreshCw,Heart,MapPin,Network,Copy,Send,House,Globe2,Moon,Sun,Skull,Clock3 } from 'lucide-react';
import { CheckCircle2,LayoutDashboard,Users,BookOpen,Compass,Server,Activity,Brain,Sparkles,Plus,Play,Pause,ArrowRight,Shield,Terminal,ScrollText,Radio,RefreshCw,Heart,MapPin,Network,Copy,Send,House,Globe2,Moon,Sun,Skull,Clock3 } from 'lucide-react';
import { SidebarProvider,Sidebar,SidebarHeader,SidebarContent,SidebarMenu,SidebarMenuItem,SidebarMenuButton,SidebarFooter,SidebarTrigger } from '@/components/ui/sidebar';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -18,7 +19,7 @@ import { Table,TableHeader,TableRow,TableHead,TableBody,TableCell } from '@/comp
import { Skeleton } from '@/components/ui/skeleton';
import { Toaster,toast } from 'sonner';
import type { State,Bot,Recipe,RealmEvent,Activity as BotActivity } from '@/core/types';
const NAV=[['Dashboard',LayoutDashboard],['World Monitor',Globe2],['Nightly Report',ScrollText],['Bot Activity',Clock3],['Social Life',Heart],['System Health',Activity],['Realm Director',Compass],['World & Housing',House],['Bots',Users],['Bot Recipes',BookOpen],['Memory & Bonds',Heart],['Event Stream',Activity],['GM Console',Terminal],['Server & Models',Server]] as const;
const NAV=[['Dashboard',LayoutDashboard],['World Monitor',Globe2],['Needs attention',Shield],['Gameplay Results',CheckCircle2],['Recovery Results',RefreshCw],['Bot Performance',Activity],['Nightly Report',ScrollText],['Bot Activity',Clock3],['Social Life',Heart],['System Health',Activity],['Realm Director',Compass],['World & Housing',House],['Bots',Users],['Bot Recipes',BookOpen],['Memory & Bonds',Heart],['Event Stream',Activity],['GM Console',Terminal],['Server & Models',Server]] as const;
const ACTIVITIES:BotActivity[]=['explore','quest','socialize','trade','rest'];
function Choice({value,onChange,items,label}:{value:string;onChange:(v:string)=>void;items:{value:string;label:string}[];label:string}){return <Select value={value} onValueChange={onChange}><SelectTrigger aria-label={label}><SelectValue/></SelectTrigger><SelectContent>{items.map(i=><SelectItem key={i.value} value={i.value}>{i.label}</SelectItem>)}</SelectContent></Select>}
const options=(items:string[])=>items.map(value=>({value,label:value}));
@@ -26,6 +27,7 @@ function Avatar({bot}:{bot:Bot}){return <div className={`avatar ${bot.role}`} ar
function Panel({title,icon:Icon,children,action,className=''}:any){return <section className={`panel ${className}`}><div className="panel-head"><h2>{Icon&&<Icon/>}{title}</h2>{action}</div>{children}</section>}
function Events({events}:{events:RealmEvent[]}){return <div>{events.length?[...events].reverse().map(e=><div className="event-row" key={e.id}><span className="event-time">{new Date(e.at).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}</span><span className={`event-kind ${e.kind}`}>{e.kind}</span><span>{e.message}</span></div>):<div className="empty-note">No events match this filter.</div>}</div>}
export default function Home({readOnly=false}:{readOnly?:boolean}){
const [focusName,setFocusName]=useState('');
const [state,setState]=useState<State|null>(null),[page,setPage]=useState('Dashboard'),[error,setError]=useState(''),[busy,setBusy]=useState(false),[query,setQuery]=useState(''),[filter,setFilter]=useState('all'),[botId,setBotId]=useState<string|null>(null),[createOpen,setCreateOpen]=useState(false),[recipe,setRecipe]=useState<Recipe|null>(null),[recipeOpen,setRecipeOpen]=useState(false),[confirm,setConfirm]=useState<{title:string;description:string;action:any}|null>(null),[capabilities,setCapabilities]=useState<any>({}),[monitor,setMonitor]=useState<any>(null);
const refresh=useCallback(async()=>{try{const r=await fetch('/api/state');const d:any=await r.json();if(!r.ok)throw new Error(d.error);setState(d);setError('');}catch(e){setError(e instanceof Error?e.message:'Could not load realm');}},[]);
useEffect(()=>{void refresh();void fetch('/api/health').then(r=>r.json()).then(setCapabilities).catch(()=>{});const timer=setInterval(refresh,5000);return()=>clearInterval(timer);},[refresh]);
@@ -35,7 +37,7 @@ export default function Home({readOnly=false}:{readOnly?:boolean}){
useEffect(()=>{if(readOnly)return;const context=(document as any).modelContext;if(!context?.registerTool)return;const controller=new AbortController();const register=(tool:any)=>{try{Promise.resolve(context.registerTool(tool,{signal:controller.signal})).catch(()=>{});}catch{}};
register({name:'azerothai_read_realm',description:'Read current bot identities, recipes, director settings and recent events.',inputSchema:{type:'object',properties:{},additionalProperties:false},annotations:{readOnlyHint:true},execute:async()=>{const r=await fetch('/api/state');if(!r.ok)throw new Error('Realm unavailable');return r.json();}});
register({name:'azerothai_create_bot',description:'Create an offline bot identity; does not spawn it in game.',inputSchema:{type:'object',properties:{name:{type:'string'},role:{type:'string'},race:{type:'string'},recipeId:{type:'string'},goal:{type:'string'}},required:['name','role','race','recipeId','goal'],additionalProperties:false},annotations:{readOnlyHint:false},execute:async(input:any)=>act({...input,type:'createBot'})});return()=>controller.abort();},[act,readOnly]);
function nav(p:string){setPage(p);setQuery('');setFilter('all');}
function nav(p:string,name=''){setPage(p);setQuery('');setFilter('all');setFocusName(name);}
const selected=state?.bots.find(b=>b.id===botId);
const visible=state?.bots.filter(b=>(`${b.name} ${b.role} ${b.zone}`.toLowerCase().includes(query.toLowerCase()))&&(filter==='all'||b.status===filter))??[];
function botTable(bots:Bot[]){return <Table className="data-table"><TableHeader><TableRow><TableHead>Character</TableHead><TableHead>Status</TableHead><TableHead>Location</TableHead><TableHead>Recipe</TableHead><TableHead/></TableRow></TableHeader><TableBody>{bots.map(b=><TableRow key={b.id} onClick={()=>setBotId(b.id)}><TableCell><div className="bot-name"><Avatar bot={b}/><div><button onClick={()=>setBotId(b.id)}><strong>{b.name}</strong></button><small>Lv. {b.level} · {b.race} {b.role}</small></div></div></TableCell><TableCell><span className={`tag ${b.status}`}>{b.status}</span></TableCell><TableCell>{b.zone}</TableCell><TableCell><small>{b.personality.name.replace('The ','')}</small></TableCell><TableCell><ArrowRight size={15}/></TableCell></TableRow>)}</TableBody></Table>}
@@ -43,7 +45,8 @@ export default function Home({readOnly=false}:{readOnly?:boolean}){
<div className="page-heading"><div><div className="eyebrow">{page==='GM Console'?'THE WORLD IS YOUR SANDBOX':'AZEROTHAI CONTROL PLANE'}</div><h1>{page==='Dashboard'?'Your realm, alive.':page}</h1><p className="subtitle">{({'Dashboard':'The people, personalities, and stories that make your world.','World Monitor':'Watch Azeroth breathe: population, activity, systems, and the lives unfolding right now.','Realm Director':'Shape the adventure around you. Let your characters bring it to life.','World & Housing':'Homes, routines, simulation bands, and believable population flow.','Bots':'Give every character a purpose, then let their story unfold.','Bot Recipes':'Same model. Different personalities. A world of possibilities.','Memory & Bonds':'A shared history makes the next encounter matter.','Event Stream':'Follow the decisions and changes happening across your realm.','GM Console':'Pull up a chair. Put on the blue robe. Run your world.','Server & Models':'Connect the game engine and the minds behind your characters.'} as any)[page]}</p></div><div className="actions">{readOnly?<Button variant="outline" onClick={()=>void refresh()}><RefreshCw size={15}/>Refresh</Button>:page==='Bot Recipes'?<Button onClick={()=>{setRecipe(null);setRecipeOpen(true)}}><Plus size={16}/>Create recipe</Button>:<><Button variant="outline" disabled={busy} onClick={()=>void refresh()}><RefreshCw size={15}/>Refresh</Button><Button onClick={()=>setCreateOpen(true)}><Plus size={16}/>Create bot</Button></>}</div></div>
{error&&<div className="notice" role="alert">{error} <Button variant="outline" onClick={()=>void refresh()}>Retry</Button></div>}
{!state?<div className="stats">{[1,2,3,4].map(n=><Skeleton key={n} className="h-32"/>)}</div>:<>
{(page==='Nightly Report'||page==='Bot Activity')&&<RealmInsights mode={page}/>}
{['Needs attention','Gameplay Results','Recovery Results','Bot Performance'].includes(page)&&<RealmDiagnostics key={page+focusName} page={page} initialQuery={focusName} onNavigate={nav}/>}
{(page==='Nightly Report'||page==='Bot Activity')&&<RealmInsights key={page+focusName} mode={page} initialQuery={focusName}/>}
{page==='Dashboard'&&<><div className="stats">{[[Users,'Active bots',state.bots.filter(b=>b.status==='online'&&(state.settings.connector==='simulation'||b.autonomy?.managed)).length,'Online in the simulation'],[BookOpen,'Bot recipes',state.recipes.length,'Distinct ways to experience the world'],[Brain,'Memories',state.bots.reduce((n,b)=>n+b.memories.length,0),'Stories carried into future decisions'],[Network,'Relationships',state.bots.reduce((n,b)=>n+b.relationships.length,0),'Directed bonds between characters']].map(([Icon,label,value,note]:any)=><div className="panel stat" key={label}><div className="stat-label">{label}<Icon/></div><div className="stat-value">{value}</div><small>{label==='Active bots'&&state.settings.connector!=='simulation'?'Acknowledged by the game bridge':note}</small></div>)}</div><section className="panel director-strip"><div className="director-content"><Compass className="director-icon"/><div><div className="eyebrow">YOUR NEXT ADVENTURE</div><h2>{state.director.experience}</h2><p>{state.director.zone} · {state.director.brief}</p></div></div><Button variant="outline" onClick={()=>nav('Realm Director')}>{readOnly?'View realm direction':'Shape the experience'} <ArrowRight size={15}/></Button></section><div className="dashboard-grid"><Panel title="Your characters" icon={Users} action={<Button variant="ghost" size="sm" onClick={()=>nav('Bots')}>{readOnly?'View bots':'Manage bots'} <ArrowRight size={14}/></Button>}>{botTable(state.bots.slice(0,5))}</Panel><Panel title="Realm pulse" icon={Radio}><div className="panel-body">{Array.from(new Set(state.bots.filter(b=>b.status==='online').map(b=>b.zone))).map(zone=>{const n=state.bots.filter(b=>b.status==='online'&&b.zone===zone).length;return <div className="zone-row" key={zone}><span className="zone-label">{zone}</span><span className="bar"><i style={{width:`${n/Math.max(1,state.bots.filter(b=>b.status==='online').length)*100}%`}}/></span><span>{n}</span></div>})}<p className="fineprint">Bot locations from the selected connector. Simulation locations are illustrative.</p><div className="engine-box" style={{marginTop:25,marginBottom:0}}><Brain size={23}/><div><h3>{state.settings.provider==='deterministic'?'Personality simulator':state.settings.model}</h3><p>{state.settings.provider==='deterministic'?'No model required. Try a decision from any online bot.':'Local model selected. Each decision includes identity and memories.'}</p></div></div></div></Panel><Panel title="Recent activity" icon={Activity} action={<Button variant="ghost" size="sm" onClick={()=>nav('Event Stream')}>View stream <ArrowRight size={14}/></Button>}>{<Events events={state.events.slice(-5)}/>}</Panel><Panel title="The Game Master’s chair" icon={Shield}><div className="panel-body"><p className="list-muted">Set the scene. Announce a gathering. Move a character to the next chapter of their story.</p><div className="gm-preview"><span>&lt;GM&gt; AzerothAI</span><p>Welcome to your world.</p><code>.announce An adventure awaits...</code></div><Button variant="outline" onClick={()=>nav('GM Console')}>Open GM console <Terminal size={15}/></Button></div></Panel></div></>}
{page==='World Monitor'&&<WorldMonitor state={state} monitor={monitor} capabilities={capabilities} openBot={setBotId}/>}
{page==='Bots'&&<><div className="toolbar"><Input aria-label="Search bots" placeholder="Search name, class, or location…" value={query} onChange={e=>setQuery(e.target.value)}/><Choice label="Bot status" value={filter} onChange={setFilter} items={options(['all','online','paused','offline','retired'])}/><small>{visible.length} characters</small></div><Panel title="Bot lifecycle" icon={Users}>{visible.length?botTable(visible):<div className="empty-note">No matching bots. Create a character or adjust your filters.</div>}</Panel></>}