'use client';
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 { 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';
import { Dialog,DialogContent,DialogHeader,DialogTitle,DialogDescription } from '@/components/ui/dialog';
import { Sheet,SheetContent,SheetHeader,SheetTitle,SheetDescription } from '@/components/ui/sheet';
import { AlertDialog,AlertDialogContent,AlertDialogHeader,AlertDialogTitle,AlertDialogDescription,AlertDialogFooter,AlertDialogCancel,AlertDialogAction } from '@/components/ui/alert-dialog';
import { Tabs,TabsList,TabsTrigger,TabsContent } from '@/components/ui/tabs';
import { Select,SelectTrigger,SelectValue,SelectContent,SelectItem } from '@/components/ui/select';
import { Slider } from '@/components/ui/slider';
import { Checkbox } from '@/components/ui/checkbox';
import { Switch } from '@/components/ui/switch';
import { Table,TableHeader,TableRow,TableHead,TableBody,TableCell } from '@/components/ui/table';
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],['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 {items.map(i=>{i.label} )} }
const options=(items:string[])=>items.map(value=>({value,label:value}));
function Avatar({bot}:{bot:Bot}){return
{bot.name.slice(0,1)}
}
function Panel({title,icon:Icon,children,action,className=''}:any){return
{Icon&& }{title} {action}{children} }
function Events({events}:{events:RealmEvent[]}){return {events.length?[...events].reverse().map(e=>
{new Date(e.at).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})} {e.kind} {e.message}
):
No events match this filter.
}
}
export default function Home({readOnly=false}:{readOnly?:boolean}){
const [state,setState]=useState(null),[page,setPage]=useState('Dashboard'),[error,setError]=useState(''),[busy,setBusy]=useState(false),[query,setQuery]=useState(''),[filter,setFilter]=useState('all'),[botId,setBotId]=useState(null),[createOpen,setCreateOpen]=useState(false),[recipe,setRecipe]=useState(null),[recipeOpen,setRecipeOpen]=useState(false),[confirm,setConfirm]=useState<{title:string;description:string;action:any}|null>(null),[capabilities,setCapabilities]=useState({}),[monitor,setMonitor]=useState(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]);
useEffect(()=>{if(page!=='World Monitor')return;let cancelled=false;const load=async()=>{try{const r=await fetch('/api/world-monitor');const d=await r.json();if(!cancelled)setMonitor(d);}catch{if(!cancelled)setMonitor(null);}};void load();const timer=setInterval(load,3000);return()=>{cancelled=true;clearInterval(timer);};},[page]);
const act=useCallback(async(action:any)=>{if(readOnly)throw new Error('Visitor access is read-only');setBusy(true);try{const r=await fetch('/api/actions',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(action)});const d:any=await r.json();if(!r.ok)throw new Error(d.error);await refresh();return d;}catch(e){toast.error(e instanceof Error?e.message:'Action failed');await refresh();throw e;}finally{setBusy(false);}},[refresh,readOnly]);
const perform=(action:any)=>{void act(action).catch(()=>{});};
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');}
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 Character Status Location Recipe {bots.map(b=>setBotId(b.id)}>setBotId(b.id)}>{b.name} Lv. {b.level} · {b.race} {b.role}
{b.status} {b.zone} {b.personality.name.replace('The ','')} )}
}
return A AzerothAI YOUR WORLD. ALIVE.
REALM MANAGEMENT
{NAV.map(([name,Icon])=>nav(name)}>{name} )} {readOnly?'Realm visitor · Read only':'Realm administrator'}
{readOnly?'Live realm monitor':'Prototype 0.1 · Private realm'} “What if Azeroth never felt empty?”
{page==='GM Console'?'THE WORLD IS YOUR SANDBOX':'AZEROTHAI CONTROL PLANE'}
{page==='Dashboard'?'Your realm, alive.':page} {({'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]}
{readOnly?
void refresh()}>Refresh :page==='Bot Recipes'?
{setRecipe(null);setRecipeOpen(true)}}>Create recipe :<>
void refresh()}>Refresh setCreateOpen(true)}>Create bot >}
{error&&
{error} void refresh()}>Retry
}
{!state?
{[1,2,3,4].map(n=> )}
:<>
{page==='Dashboard'&&<>
{[[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)=>
{label}
{value}
{label==='Active bots'&&state.settings.connector!=='simulation'?'Acknowledged by the game bridge':note} )}
YOUR NEXT ADVENTURE
{state.director.experience} {state.director.zone} · {state.director.brief}
nav('Realm Director')}>{readOnly?'View realm direction':'Shape the experience'} nav('Bots')}>{readOnly?'View bots':'Manage bots'} }>{botTable(state.bots.slice(0,5))} {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
{zone} b.status==='online').length)*100}%`}}/> {n}
})}
Bot locations from the selected connector. Simulation locations are illustrative.
{state.settings.provider==='deterministic'?'Personality simulator':state.settings.model} {state.settings.provider==='deterministic'?'No model required. Try a decision from any online bot.':'Local model selected. Each decision includes identity and memories.'}
nav('Event Stream')}>View stream }>{} Set the scene. Announce a gathering. Move a character to the next chapter of their story.
<GM> AzerothAI Welcome to your world.
.announce An adventure awaits...nav('GM Console')}>Open GM console >}
{page==='World Monitor'&&
}
{page==='Bots'&&<> setQuery(e.target.value)}/>{visible.length} characters
{visible.length?botTable(visible):No matching bots. Create a character or adjust your filters.
} >}
{page==='Bot Recipes'&&<>Recipes are versioned. Saving a recipe preserves existing bots’ personalities until you explicitly apply the new version.
{state.recipes.map((r,i)=>
{setRecipe(r);setRecipeOpen(true)}}>{i%2?:} VERSION {r.version}
{r.name} {r.description}
{r.tags.map(t=>{t} )}
{state.bots.filter(b=>b.recipeId===r.id).length} bot identities {readOnly?'View recipe →':'Edit recipe →'}
)}
>}
{page==='Realm Director'&& }
{page==='World & Housing'&& }
{page==='Memory & Bonds'&&{state.bots.map(b=>
setBotId(b.id)}>Open profile}>{b.memories.length} memories · {b.relationships.length} relationships
{b.memories.at(-1)?.text??'No memories yet.'}
{b.relationships.map(r=>
{state.bots.find(x=>x.id===r.targetId)?.name} · {r.affinity>0?'+':''}{r.affinity} affinity {r.note}
)}
)}
}
{page==='Social Life'&&}
{page==='System Health'&&}
{page==='Event Stream'&&}
{page==='Server & Models'&& }
{page==='GM Console'&& }
>}
AzerothAI shapes identity and intent. AzerothCore owns the rules of the world.
Create a character Start with an identity and recipe. Spawn when you’re ready. {state&&setCreateOpen(false)}/>}
{readOnly?'Bot recipe':recipe?'Edit bot recipe':'Create bot recipe'} Traits guide high-level decisions. They never override game rules. setRecipeOpen(false)}/>
{if(!o)setBotId(null)}}>Bot profile Identity, intent, and the story so far. {selected&&state&&}
{if(!o)setConfirm(null)}}>{confirm?.title} {confirm?.description} Cancel {if(confirm)perform(confirm.action);setConfirm(null)}}>Confirm ;
}
function CreateBot({state,act,busy,close}:any){const [name,setName]=useState(''),[role,setRole]=useState('Priest'),[race,setRace]=useState('Human'),[recipeId,setRecipeId]=useState(state.recipes[0].id),[goal,setGoal]=useState(`Support ${state.director.experience.toLowerCase()} in ${state.director.zone}`);return }
function RecipeEditor({recipe,act,busy,close}:any){const [r,setR]=useState(recipe??{name:'',description:'',tags:['Curious'],sociability:50,aggression:20,curiosity:70,economy:30,activities:['explore','quest']});const [tags,setTags]=useState(r.tags.join(', '));return {e.preventDefault();try{await act({type:'saveRecipe',recipe:{...r,tags:tags.split(',').map(t=>t.trim()).filter(Boolean)}});toast.success('Recipe saved');close();}catch{}}}>Recipe name setR({...r,name:e.target.value})}/> Personality descriptionsetR({...r,description:e.target.value})}/> Tags, separated by commas setTags(e.target.value)}/>
{(['sociability','aggression','curiosity','economy'] as const).map(key=>
{key} {r[key]}
setR({...r,[key]:v[0]})}/> )}
Preferred activities {ACTIVITIES.map(a=>setR({...r,activities:v?[...r.activities,a]:r.activities.filter(x=>x!==a)})}/>{a} )}
{recipe&&{try{const {id,version,...copy}=r;void id;void version;await act({type:'saveRecipe',recipe:{...copy,name:`${copy.name} copy`.slice(0,80)}});close();}catch{}}}>Duplicate }Save recipe
}
function BotProfile({bot:b,state,act,busy,perform,confirm,readOnly=false}:any){const [memory,setMemory]=useState(''),[importance,setImportance]=useState(7),[target,setTarget]=useState(state.bots.find((x:Bot)=>x.id!==b.id)?.id??''),[affinity,setAffinity]=useState(40),[note,setNote]=useState(''),[recipeId,setRecipeId]=useState(b.recipeId);const actions=b.status==='online'?['pause','despawn']:b.status==='paused'?['resume','despawn']:b.status==='offline'?['spawn','retire']:[];return <>{b.name} Level {b.level} {b.race} {b.role}
{b.status} {actions.map(a=>
a==='retire'?confirm({title:`Retire ${b.name}?`,description:'The identity and memories remain in your realm, but this bot cannot spawn again in this prototype.',action:{type:'lifecycle',botId:b.id,action:a}}):perform({type:'lifecycle',botId:b.id,action:a})}>{a==='pause'?:a==='spawn'||a==='resume'?:null}{a.charAt(0).toUpperCase()+a.slice(1)} )}
perform({type:'think',botId:b.id})}>{busy?'Working…':'Run decision'} Identity Memory Relationships Current goal {b.goal}
{b.zone}
{b.personality.name} v{b.recipeVersion} {b.personality.description}
{b.personality.tags.map((t:string)=>{t} )}
({value:r.id,label:`${r.name} · v${r.version}`}))}/>perform({type:'applyRecipe',botId:b.id,recipeId})}>Apply current recipe version Latest decision {b.lastDecision?`${b.lastDecision.action}: ${b.lastDecision.reason}`:'No decisions yet. Run a decision while this bot is online.'}
{b.decisions} decisions acknowledged {e.preventDefault();try{await act({type:'memory',botId:b.id,text:memory,importance});setMemory('');}catch{}}}>Add a memorysetMemory(e.target.value)} placeholder="What should this character remember?"/> Importance {importance}/10
setImportance(v[0])}/> Save memory {[...b.memories].reverse().map((m:any)=>{m.text}
{m.source} · Importance {m.importance}/10 · {new Date(m.at).toLocaleDateString()} )}Bonds are directional. One character’s trust does not imply the other’s.
{e.preventDefault();try{await act({type:'relationship',botId:b.id,targetId:target,affinity,note});setNote('');}catch{}}}>Other character x.id!==b.id).map((x:Bot)=>({value:x.id,label:x.name}))}/>
Affinity {affinity>0?'+':''}{affinity}
setAffinity(v[0])}/> Why this bond existssetNote(e.target.value)}/> Save relationship {b.relationships.map((r:any)=>{state.bots.find((x:Bot)=>x.id===r.targetId)?.name} {r.affinity>0?'+':''}{r.affinity} {r.note}
)} >}
function WorldMonitor({state,monitor,capabilities,openBot}:any){
const living:Bot[]=state.bots.filter((b:Bot)=>b.status!=='dead'&&b.status!=='retired');
const active=living.filter((b:Bot)=>b.status==='online');
const managed=living.filter((b:Bot)=>b.autonomy?.managed===true);
const foreground=living.filter((b:Bot)=>b.simulationBand==='foreground');
const background=living.filter((b:Bot)=>b.simulationBand==='background');
const dormant=living.filter((b:Bot)=>b.simulationBand==='dormant');
const dead=state.bots.filter((b:Bot)=>b.status==='dead');
const zaku=state.bots.find((b:Bot)=>b.id==='hero-zaku'||(b.tier==='hero'&&b.name.toLowerCase()==='zaku'));
const zoneMap=new Map();
for(const b of living){
const z=zoneMap.get(b.zone)??{total:0,foreground:0,background:0,dormant:0,names:[]};
z.total++;z[b.simulationBand]++;z.names.push(b.name);zoneMap.set(b.zone,z);
}
const zones=[...zoneMap.entries()].sort((a,b)=>b[1].total-a[1].total);
const worldEvents=[...(state.events??[])].reverse().slice(0,14);
const aleEvents=monitor?.events??[];
const lastAle=monitor?.ale?.lastEventAt?new Date(monitor.ale.lastEventAt):null;
const aleFresh=!!monitor?.ale?.activeRecently;
const systemRows=[
['AzerothCore',aleFresh?'Live':(capabilities.aleConfigured?'Waiting for telemetry':'Not configured'),aleFresh],
['ALE',capabilities.aleConfigured?(aleFresh?'Live':'Configured'):'Not configured',capabilities.aleConfigured&&aleFresh],
['Playerbots',managed.length>0?`${managed.length} persistent lives bound`:'Awaiting live bot telemetry',managed.length>0],
['AI brain',state.settings.provider==='deterministic'?'Deterministic':(capabilities.llmConfigured?state.settings.brains.heroModel:'Endpoint missing'),state.settings.provider==='deterministic'||capabilities.llmConfigured],
['GM SOAP',capabilities.soapConfigured?'Ready':'Not configured',!!capabilities.soapConfigured],
['Population Director',state.population.lastPlan?'Active':'Waiting for trigger',!!state.population.lastPlan]
];
return
{[
[Users,'Living residents',living.length,`${managed.length} live-bound · ${active.length} online`],
[Sun,'Foreground',foreground.length,'Fully simulated nearby'],
[Moon,'Background + dormant',background.length+dormant.length,`${background.length} background · ${dormant.length} dormant`],
[Skull,'Memorialized deaths',dead.length,state.settings.hardcore.enabled?'Hardcore enabled':'Hardcore disabled']
].map(([Icon,label,value,note]:any)=>
)}
{zaku?openBot(zaku.id)}>
{zaku.name} Lv. {zaku.level} {zaku.race} {zaku.role}
{zaku.status}
Location {zaku.zone}
Simulation {zaku.simulationBand}
Home {state.homes.find((h:any)=>h.id===zaku.homeId)?.name??'Unassigned'}
Current goal {zaku.goal}
Live body {zaku.autonomy?.managed?'Observed Playerbot':'Awaiting binding'}
Decisions {zaku.decisions}
{zaku.autonomy?.nextIntentAt&&Next life decision {new Date(zaku.autonomy.nextIntentAt).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}
}
{zaku.lastDecision&&LATEST DECISION {zaku.lastDecision.action}: {zaku.lastDecision.reason}
}
Tap Zaku to open his full identity, memories, bonds, and controls.
:Zaku is not present in this realm state.
}
{systemRows.map(([name,status,ok]:any)=>
{name} {status}
)}
ALE last world event {lastAle?lastAle.toLocaleTimeString():'None yet'}
ALE command queue {monitor?`${monitor.commands?.pending??0} pending · ${monitor.commands?.failed??0} failed`:'Loading…'}
“Live” means recent authoritative ALE activity reached AzerothAI. Configured alone does not mean the worldserver is currently online.
{zones.length?zones.map(([zone,z])=>
{zone} {z.total}
{z.foreground} fg · {z.background} bg · {z.dormant} sleep
{z.names.slice(0,6).join(', ')}{z.names.length>6?` +${z.names.length-6} more`:''} ):
No living residents have a recorded location yet.
}
Server target {state.settings.population.serverMax}
Zone target {state.settings.population.defaultZoneTarget}
Foreground ceiling {state.settings.population.foregroundMax}
Background ceiling {state.settings.population.backgroundMax}
{state.population.lastPlan?<>
LATEST DIRECTOR PLAN {state.population.lastPlan.trigger.kind} in {state.population.lastPlan.trigger.zone}
{state.population.lastPlan.selectedBotIds.length} resident(s) selected · {state.population.lastPlan.requested} requested.
>:
No population plan has fired yet.
}
{worldEvents.length?worldEvents.map((e:RealmEvent)=>
{new Date(e.at).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'})} {e.kind} {e.message}
):
The realm event feed is quiet.
}
{aleEvents.length?aleEvents.slice(0,12).map((e:any)=>
{new Date(e.at).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'})} ALE {e.kind}{e.name?` · ${e.name}`:''}{e.zone?` · ${e.zone}`:''}{e.isBot?' · Playerbot':''}
):
No authoritative ALE world events have arrived yet. When the playable realm is online, login, combat, zone, quest, weather, and other server events will appear here.
}
}
function WorldHousing({state,act,busy}:any){
const [kind,setKind]=useState('zone-change'),[zone,setZone]=useState(state.director.zone),[playerName,setPlayerName]=useState(state.director.playerName??''),[detail,setDetail]=useState('');
const plan=state.population.lastPlan;
const foreground=state.bots.filter((b:Bot)=>b.simulationBand==='foreground'&&b.status!=='dead'&&b.status!=='retired').length;
const background=state.bots.filter((b:Bot)=>b.simulationBand==='background'&&b.status!=='dead'&&b.status!=='retired').length;
const dormant=state.bots.filter((b:Bot)=>b.simulationBand==='dormant'&&b.status!=='dead'&&b.status!=='retired').length;
return <>{[[House,'Homes',state.homes.length,'Persistent places residents return to'],[Users,'Foreground',foreground,`Max ${state.settings.population.foregroundMax}`],[Activity,'Background',background,`Max ${state.settings.population.backgroundMax}`],[Pause,'Dormant',dormant,'Sleeping or cheaply simulated']].map(([Icon,label,value,note]:any)=>
)}
{e.preventDefault();try{await act({type:'worldTrigger',kind,zone,playerName,detail});toast.success('World trigger evaluated');}catch{}}}>Trigger Zone setZone(e.target.value)}/> Player name setPlayerName(e.target.value)}/> ContextsetDetail(e.target.value)} placeholder="What happened in the world?"/>
Evaluate population
AzerothAI prefers existing residents with a believable reason to be here. It does not create disposable identities to fill a zone.
{plan?<>
Trigger {plan.trigger.kind} · {plan.trigger.zone}
Zone target {plan.target}
Already active {plan.activeBefore}
Requested {plan.requested}
Capacity available {plan.capacity}
Residents selected {plan.selectedBotIds.length}
{plan.selectedBotIds.length>0&&
{plan.selectedBotIds.map((id:string)=>state.bots.find((b:Bot)=>b.id===id)?.name).filter(Boolean).join(', ')}
}{plan.rejectedReasons.map((x:string)=>
{x}
)}>:
No population trigger has been evaluated yet.
}
Home Zone Type Residents Safety {state.homes.map((h:any)=>{h.name} {h.description} {h.zone} {h.type} {h.residentIds.map((id:string)=>state.bots.find((b:Bot)=>b.id===id)?.name).filter(Boolean).join(', ')||'Vacant'} ({h.residentIds.length}/{h.capacity}) {h.safety}/100 )}
>
}
function Director({state,act,busy}:any){const [d,setD]=useState(state.director);return {e.preventDefault();try{await act({type:'director',...d});toast.success('Direction saved and bot goals updated');}catch{}}}>What kind of session do you want?setD({...d,experience})} items={options(['Adventuring companions','A lively town','Exploration and discovery','Rivalries and tension'])}/> Your character name setD({...d,playerName:e.target.value})} placeholder="Optional"/> Focus zone setD({...d,zone:e.target.value})}/> Your directionsetD({...d,brief:e.target.value})} style={{minHeight:140}}/>
Apply realm direction
Updates the goals of all non-retired bots. Run individual decisions from a bot profile to act on this direction.
Characters, not background noise A healer who remembers you. A trader with an agenda. A familiar rival on the road.
Direction, not a scripted outcome Your brief goes into bot goals. Personality, memory, and available actions influence their next choice.
The game stays the game The game connector translates intent. AzerothCore resolves movement, combat, loot, and progression.
Live Playerbots continue playing under AzerothCore even with no human online. AzerothAI periodically adds personality-, memory-, and goal-driven high-level intentions for authoritatively observed residents.
}
function EventStream({state,readOnly=false}:any){const [events,setEvents]=useState(state.events),[paused,setPaused]=useState(false),[kind,setKind]=useState('all'),[connection,setConnection]=useState('Connecting');useEffect(()=>{if(paused){setConnection('Paused');return;}setEvents(state.events);if(readOnly){setConnection('Live · read only');return;}const source=new EventSource(`/api/events?after=${state.sequence}`);source.addEventListener('realm',(e:MessageEvent)=>{const event=JSON.parse(e.data);setEvents(old=>old.some(x=>x.id===event.id)?old:[...old,event].slice(-500));setConnection('Connected');});source.addEventListener('gap',()=>void fetch('/api/state').then(r=>r.json()).then((s:any)=>setEvents(s.events)));source.onopen=()=>setConnection('Connected');source.onerror=()=>setConnection('Reconnecting');return()=>source.close();},[paused,readOnly,state.sequence]);return <>setPaused(!paused)}>{paused?:} {paused?'Resume stream':'Pause stream'} {connection} Latest 500 events · replay on reconnect}>kind==='all'||e.kind===kind)}/> >}
function Settings({state,act,busy,capabilities}:any){
const [s,setS]=useState(state.settings),[health,setHealth]=useState('');
const brains=s.brains;
const setBrains=(patch:any)=>setS({...s,brains:{...brains,...patch}});
return
Game connector setS({...s,connector})} items={[{value:'simulation',label:'Simulation · Ready to use'},{value:'azerothcore',label:'AzerothCore · HTTP bridge'}]}/>
Game bridge configured {capabilities.bridgeConfigured?'Yes':'Not yet'}
ALE control plane {capabilities.aleConfigured?'Configured':'Not yet'}
GM SOAP configured {capabilities.soapConfigured?'Yes':'Not yet'}
Mechanics authority AzerothCore
{try{const r=await act({type:'health'});setHealth(r.health.detail);}catch{setHealth('Connection test failed. Check server configuration.')}}}>Test saved connector {health&&
{health}
}
AzerothAI Identity → memory retrieval → brain routing → high-level intent
AzerothCore + ALE Movement → combat → inventory → world rules → authoritative events
}
function GM({state,act,busy,capabilities,setConfirm}:any){const [command,setCommand]=useState('.server info'),[live,setLive]=useState(false),[history,setHistory]=useState([]),[index,setIndex]=useState(-1);const gmEvents=state.events.filter((e:RealmEvent)=>e.kind==='gm'||e.kind==='error').slice(-30);const shortcuts=[['Server info','.server info'],['World announcement','.announce Travelers, gather at the gates of Stormwind!'],['Teleport character','.teleport name Yourname Stormwind'],['Character info','.pinfo Yourname'],['Message of the day','.server motd']];return {live?'LIVE COMMANDS':'SIMULATED COMMANDS'}}><GM> AZEROTHAIRealm operations terminal · Command channel ready
{live?'Commands will execute on your configured worldserver.':'Simulation mode. No connection to a real worldserver.'}
{gmEvents.length?gmEvents.map((e:RealmEvent)=>
{new Date(e.at).toLocaleTimeString()} {e.message} ):
Type .server info to get started.
}
{e.preventDefault();if(!command.trim())return;setHistory(h=>[command,...h].slice(0,30));setIndex(-1);if(live){setConfirm({title:'Execute on the live worldserver?',description:command,action:{type:'gm',command,live:true,confirmed:true}});return;}try{await act({type:'gm',command,live:false});setCommand('');}catch{}}}>> setCommand(e.target.value)} onKeyDown={e=>{if(e.key==='ArrowUp'){e.preventDefault();const n=Math.min(index+1,history.length-1);setIndex(n);if(n>=0)setCommand(history[n]);}if(e.key==='ArrowDown'){e.preventDefault();const n=Math.max(-1,index-1);setIndex(n);setCommand(n>=0?history[n]:'');}}}/> Use live worldserver
{shortcuts.map(([label,cmd])=>
setCommand(cmd)}>{label} {cmd.split(' ').slice(0,label==='Teleport character'?2:1).join(' ')} )}
Live commands use AzerothCore SOAP and require confirmation. Supported commands are limited to this toolbox; bot models cannot issue them.
In-game commands such as .gm on, .gm visible off, and .summon need your logged-in GM character. This web console has no character position or selected target.
The first prototype provides remote announcements, named teleports, and server/player information. More GM powers can be added explicitly.
}