src/blackrock/BlackrockWorkspace.tsx
import {navigateSection} from './navigation';
import {ItemSearchInput} from './ItemSearchInput';
import {BankHoldings} from './BankHoldings';
import {Button} from '@whop/react/components';
import {EconomyOverview} from './EconomyOverview';
import {researchPrompt} from './research-interactions';
import {useEffect,useRef,useState} from 'react';
import {Search,Star,ArrowLeft,ArrowUpRight,RefreshCw,Download,Upload,ChevronRight} from 'lucide-react';
import {api,mutate,useResource,number,time,download,relationships,type Entity,type Relationship} from './api';
import {ItemArt} from './ItemArt';
import {useWatchQuotes,WatchQuotesNotice,WatchQuoteSummary} from './WatchQuotes';
import {Empty,ErrorMessage,Facts,SourceState,History,Production,OpportunityScan} from './Research';
import './blackrock.css';
import './intelligence-flow.css';
import {MarketDashboard,MarketCharts} from './MarketCharts';
import {MarketBoard,HolderSummary} from './MarketBoard';
import {makeImportBatches,quoteRecord} from './model';
import {researchSection,researchSections,readLanding,type ResearchSection as Section} from './navigation';
import {ResearchNavigation,ResearchSubnavigation,EconomicsResearch,ResearchApiTools,SourcePayload,OriginalReport} from './ResearchTools';
const keyOf=(e:Entity)=>`${e.kind}:${e.key}`;
export function BlackrockWorkspace({onAsk,onOpenWorld,onOpenSwarm,section='economy',active=true,researchRequest,onContext,onEntityContext,onNavigate,navigationId=0}:{onAsk:(prompt:string)=>void;onOpenWorld:(name?:string)=>void;onOpenSwarm?:(id:string)=>void;section?:Section;active?:boolean;researchRequest?:{entity:Entity;id:number}|null;onContext?:(label:string)=>void;onEntityContext?:(entity:Entity|null)=>void;onNavigate:(section:Section)=>void;navigationId?:number}){
const [query,setQuery]=useState(''),[debounced,setDebounced]=useState(''),[tab,setTab]=useState<'item'|'player'>(section==='fleet'?'player':'item'),[group,setGroup]=useState('all'),[fleetScope,setFleetScope]=useState('friendly'),[visibleCount,setVisibleCount]=useState(60),[selected,setSelected]=useState<Entity|null>(null),[revision,setRevision]=useState(0),[notice,setNotice]=useState(''),[mutationError,setMutationError]=useState<string|null>(null),[busy,setBusy]=useState(false),[settings,setSettings]=useState(false),[importPreview,setImportPreview]=useState<any>(null),[importBatches,setImportBatches]=useState<any[]>([]),[importName,setImportName]=useState('');
useEffect(()=>{setSelected(null);setTrail([]);setQuery('');setDebounced('');if(workspaceRef.current)workspaceRef.current.scrollTop=0;},[navigationId]);
const [trail,setTrail]=useState<Entity[]>([]);
const modeScroll=useRef(0);
const [landing,setLanding]=useState(()=>readLanding(localStorage));
const [visited,setVisited]=useState<Set<Section>>(()=>new Set([section]));
useEffect(()=>{setVisited(old=>old.has(section)?old:new Set([...old,section]));},[section]);
const workspaceRef=useRef<HTMLDivElement>(null),savedWorkspaceScroll=useRef(0),savedWindowScroll=useRef(0);
const searchRef=useRef<HTMLInputElement>(null),exchangeSearchRef=useRef<HTMLInputElement>(null),bankSearchRef=useRef<HTMLInputElement>(null),listRef=useRef<HTMLDivElement>(null),detailRef=useRef<HTMLElement>(null),lastFocus=useRef<HTMLElement|null>(null),savedScroll=useRef(0),importRef=useRef<HTMLInputElement>(null);
const swarmSearch=useResource(active&&debounced?`/api/swarms/v1/groups?q=${encodeURIComponent(debounced)}&limit=20`:null);
const swarmResult=swarmSearch.data?.data;
const fleet=useResource(section==='fleet'?'/fleet':null,revision,30000);
const watches=useResource('/watchlist',revision,30000),search=useResource(debounced&&((section!=='market'&§ion!=='holders')||selected)?`/search?q=${encodeURIComponent(debounced)}&limit=40`:null,revision),collector=useResource('/collector',revision,15000),ownership=useResource(settings?'/ownership':null,revision);
const activeItems=(watches.data?.items??[]).filter((x:any)=>x.active!==false&&x.active!==0),players=watches.data?.players??[];
useEffect(()=>{const timer=setTimeout(()=>setDebounced(query.trim()),250);return()=>clearTimeout(timer)},[query]);
useEffect(()=>{if(section==='fleet')setTab('player');if(section==='market'||section==='production')setTab('item')},[section]);
const back=()=>{setSelected(null);setTrail([]);requestAnimationFrame(()=>{if(listRef.current)listRef.current.scrollTop=savedScroll.current;if(workspaceRef.current)workspaceRef.current.scrollTop=savedWorkspaceScroll.current;window.scrollTo(0,savedWindowScroll.current);lastFocus.current?.focus({preventScroll:true})})};
useEffect(()=>{if(!active)return;const keyboard=(e:KeyboardEvent)=>{if((e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==='k'){e.preventDefault();const input=(!selected&§ion==='holders'?bankSearchRef:section==='market'&&!selected?exchangeSearchRef:searchRef).current;input?.focus();input?.select()}if(e.key==='Escape'){if(settings)setSettings(false);else if(selected)back()}};window.addEventListener('keydown',keyboard);const pop=()=>{if(location.hash===`#${section}`&&!history.state?.blackrockEntity)back()};window.addEventListener('popstate',pop);return()=>{window.removeEventListener('keydown',keyboard);window.removeEventListener('popstate',pop)}},[selected,settings,active,section]);
useEffect(()=>{onEntityContext?.(selected)},[selected,onEntityContext]);
const select=(entity:Entity)=>{if(!selected){lastFocus.current=document.activeElement as HTMLElement;savedScroll.current=listRef.current?.scrollTop??0;savedWorkspaceScroll.current=workspaceRef.current?.scrollTop??0;savedWindowScroll.current=window.scrollY;}setTrail(old=>selected?[...old,selected].slice(-12):[]);setSelected(entity);if(window.innerWidth<1280){if(history.state?.blackrockEntity)history.replaceState({...history.state,blackrockEntity:keyOf(entity)},'');else history.pushState({...history.state,blackrockEntity:keyOf(entity)},'');requestAnimationFrame(()=>{detailRef.current?.focus();detailRef.current?.scrollIntoView({block:'start'})})}};
useEffect(()=>{if(researchRequest){setTrail([]);setSelected(researchRequest.entity);}},[researchRequest]);
useEffect(()=>{onContext?.(selected?`${selected.name} · ${selected.view??'overview'}`:(query.trim()?`${researchSection(section).label}: ${query.trim()}`:researchSection(section).label));},[selected,section,query,onContext]);
useEffect(()=>{if(active&&workspaceRef.current)workspaceRef.current.scrollTop=modeScroll.current;},[active]);
const saved=(e:Entity)=>e.kind==='item'?activeItems.some((r:any)=>r.itemId===Number(e.key)):players.some((r:any)=>r.name.toLowerCase()===e.name.toLowerCase());
const action=async(fn:()=>Promise<unknown>,success:string)=>{setBusy(true);setMutationError(null);try{await fn();setNotice(success);setRevision(x=>x+1)}catch(e){setMutationError((e as Error).message)}finally{setBusy(false)}};
const toggle=(e:Entity)=>action(()=>e.kind==='item'?mutate(saved(e)?`/items/watch?itemId=${e.key}`:'/items/watch',saved(e)?'DELETE':'POST',saved(e)?undefined:{itemId:Number(e.key)}):mutate(saved(e)?`/api/watches?name=${encodeURIComponent(e.name)}`:'/api/watches',saved(e)?'DELETE':'POST',saved(e)?undefined:{name:e.name}),saved(e)?'Removed from watchlist. Existing observations remain.':'Saved. Collection runs while the local service is enabled.');
const fleetPlayers:Entity[]=fleet.data?.players??[];
const isFleet=section==='fleet'&&tab==='player';
useEffect(()=>setVisibleCount(60),[debounced,group,fleetScope,section,tab]);
const sourceRows:Entity[]=isFleet?fleetPlayers.filter(e=>!debounced||[e.name,...e.aliases??[]].some(n=>n.toLowerCase().includes(debounced.toLowerCase()))):debounced?(tab==='item'?search.data?.items:search.data?.players)??[]:tab==='item'?activeItems.map((r:any)=>({kind:'item',key:String(r.itemId),itemId:r.itemId,name:r.name??`Item ${r.itemId}`})):players.map((r:any)=>({kind:'player',key:r.name,name:r.name,relationship:r.relationship}));
const rows=sourceRows.filter(e=>(tab==='item'||group==='all'||(e.relationship??'unclassified')===group)&&(!isFleet||fleetScope==='all'||(fleetScope==='friendly'?e.friendly:e.saved)));
const showWatchQuotes=section==='watchlists'&&tab==='item'&&!query.trim()&&!debounced;
const watchQuotes=useWatchQuotes(showWatchQuotes?rows.slice(0,visibleCount).map(e=>Number(e.key)):[],active&&showWatchQuotes,revision);
const listLoading=isFleet?fleet.loading:debounced?search.loading:watches.loading;
const listError=isFleet?fleet.error:debounced?search.error:watches.error;
const previewImport=async(file:File)=>{setImportName(file.name);setImportPreview(null);setImportBatches([]);setMutationError(null);try{if(file.size>2000000)throw new Error('Import is larger than 2 MB. Split the file before importing.');const value=JSON.parse(await file.text());const batches=makeImportBatches(value);const previews=[];for(const batch of batches)previews.push(await mutate('/watchlist/import?dryRun=true','POST',batch));setImportBatches(batches);setImportPreview(previews);}catch(e){setMutationError((e as Error).message)}};
return <div className="br-flow-shell"><ResearchNavigation section={section} onNavigate={onNavigate}/><div ref={workspaceRef} onScroll={()=>{if(active&&workspaceRef.current)modeScroll.current=workspaceRef.current.scrollTop;}} className={`br-workspace br-section-${section} ${selected?'br-has-detail':''} ${showWatchQuotes?'br-watch-quotes-visible':''}`}>
<label className="br-flow-mobile-nav">Workspace<select aria-label="Blackrock workspace" value={section} onChange={e=>onNavigate(e.target.value as Section)}>{researchSections.map(s=><option key={s.key} value={s.key}>{s.label}</option>)}</select></label>
<div className="br-heading"><div><span className="br-eyebrow">ECONOMY + FLEET INTELLIGENCE</span><h1>{researchSection(section).label}</h1><p>{researchSection(section).description}. {section==='fleet'?'Your shared Palantir roster is available without saving players first.':'Research any item without adding it to a watchlist.'}</p></div><div className="br-heading-actions"><button onClick={()=>setSettings(!settings)} aria-expanded={settings}>Teams & data</button><button onClick={()=>navigateSection('settings')}>Project settings</button><button onClick={()=>onOpenWorld()}><ArrowUpRight size={17}/>Open World</button></div></div>
<ResearchSubnavigation section={section} onNavigate={onNavigate}/>
{selected&&<button className="br-workspace-return" onClick={back}><ArrowLeft size={16}/>Back to {researchSection(section).label}</button>}
{((section!=='market'&§ion!=='holders')||selected)&&<div className="br-search-bar"><label htmlFor="br-global-search">Search items, players or swarms</label><div className="br-search-input"><Search size={21}/><ItemSearchInput id="br-global-search" inputRef={searchRef} label="Search items, players or swarms" value={query} onChange={setQuery} placeholder="Steel bar, cannonball, item ID, player or swarm" onPick={item=>{setQuery(item.name);setTab('item');select({kind:'item',key:String(item.id),itemId:item.id,name:item.name})}}/>{query&&<button aria-label="Clear search" onClick={()=>setQuery('')}>Clear</button>}<kbd>⌘ K</kbd></div><Button variant="soft" aria-label={query.trim()?`Ask assistant about “${query.trim()}”`:"Ask assistant"} onClick={()=>onAsk(researchPrompt(query.trim()?`Research my search: ${query.trim()}`:'Help me find items worth buying, selling or accumulating.',{section,query:query.trim(),scope:'Search/catalog matches; verify live market quotes'},(query.trim()===debounced?rows:[]).slice(0,6).filter(e=>e.kind==='item').map(e=>({id:Number(e.key),name:e.name}))))}>Ask assistant</Button></div>}
<div className="br-service-strip"><span className={`br-dot ${collector.data?.enabled&&collector.data?.worker==='ready'?'br-dot-ready':''}`}/><span>{collector.loading?'Checking collector…':collector.error?'Collector status unavailable':!collector.data?.enabled?'Tracking paused — local service collection is disabled':collector.data?.worker==='unavailable'?'Research worker unavailable':collector.data?.running?'Collecting public observations':'Tracking while the local service runs'}</span><button onClick={()=>setSettings(!settings)}>Details</button></div>
<ErrorMessage message={mutationError}/>{notice&&<div className="br-notice" role="status">{notice}<button aria-label="Dismiss message" onClick={()=>setNotice('')}>Dismiss</button></div>}
{settings&&<section className="br-settings"><h2>Team scope & saved research</h2><label>Blackrock landing page<select value={landing} onChange={e=>{const next=e.target.value as Section;setLanding(next);try{localStorage.setItem('rs-blackrock:landing',next)}catch{}}}>{['economy','market','watchlists','holders','production','reports'].map(key=><option key={key} value={key}>{researchSection(key as Section).label}</option>)}</select></label><p className="br-muted">Choose which explicitly assigned teams belong to you. Allies and watched players stay separate from owned totals.</p><ErrorMessage message={ownership.error}/><ErrorMessage message={ownership.data?.sourceError??null}/><div className="br-team-list">{ownership.loading?<p>Loading assignments…</p>:(ownership.data?.teams??[]).length?(ownership.data.teams.map((t:any)=><label key={t.id}><input type="checkbox" disabled={busy} checked={(ownership.data.ownedTeamIds??[]).includes(t.id)} onChange={e=>void action(()=>mutate('/owned-teams','PUT',{teamIds:e.target.checked?[...ownership.data.ownedTeamIds,t.id]:ownership.data.ownedTeamIds.filter((id:string)=>id!==t.id)}),'Owned team selection updated.')}/>{t.name??t.id}</label>)):<p>No explicit team assignments available. Use a player’s manual relationship correction when needed.</p>}</div><div className="br-actions"><button onClick={()=>void action(async()=>download('rs-blackrock-watchlist.json',await api('/watchlist/export')),'Exported saved items, targets and player watches. Ownership is excluded.')}><Download size={17}/>Export watchlist</button><button onClick={()=>importRef.current?.click()}><Upload size={17}/>Import watchlist</button><input hidden type="file" accept="application/json,.json" ref={importRef} onChange={e=>{const file=e.target.files?.[0];if(file)void previewImport(file);e.target.value=''}}/></div><p className="br-muted">Import the JSON exported by your old market report. This cannot read another page’s browser storage automatically.</p>{importPreview&&<div className="br-import-preview"><h3>Import preview · {importName}</h3><p>{importBatches.length} validated batches. Existing nonempty notes and targets are preserved on conflict.</p><Facts values={[["Added",importPreview.reduce((n:number,r:any)=>n+(r.added??0),0)],["Updated",importPreview.reduce((n:number,r:any)=>n+(r.updated??0),0)],["Unchanged",importPreview.reduce((n:number,r:any)=>n+(r.unchanged??0),0)],["Conflicts",importPreview.reduce((n:number,r:any)=>n+(r.conflicts?.length??0),0)]]}/><button disabled={busy} className="br-primary" onClick={()=>void action(async()=>{let count=0;for(const batch of importBatches){await mutate('/watchlist/import','POST',batch);count++;setNotice(`Imported ${count} of ${importBatches.length} batches.`)}setImportPreview(null);setImportBatches([])},'Import complete. Saved research is synchronized with the service.')}>Import validated batches</button></div>}<h3>Collection status</h3>{collector.data&&<span className="br-muted">{collector.data.queued??0} queued · {collector.data.budget?.used??0}/{collector.data.budget?.limit??120} requests / 5 min</span>}<ErrorMessage message={collector.error}/><p className="br-muted">Target cadence: 5 minutes. Closing this page does not stop the service. Computer sleep and service downtime leave gaps. Detail lasts 90 days; daily summaries last 365 days.</p>{collector.data?.jobs?.length>0&&<div className="br-scroll-table"><table><thead><tr><th>Job</th><th>Status</th><th>Last success</th><th>Next due</th></tr></thead><tbody>{collector.data.jobs.map((j:any)=><tr key={j.jobKey}><td>{j.jobKey}{j.lastError&&<span className="br-error-inline">{j.lastError.message??String(j.lastError)}</span>}</td><td>{j.state}</td><td>{time(j.lastSucceeded)}</td><td>{time(j.nextDueAt)}</td></tr>)}</tbody></table></div>}</section>}
{debounced&§ion!=='holders'&&!selected&&onOpenSwarm&&<section className="br-swarm-search" aria-label="Matching saved swarms"><h2>Saved swarms</h2>{swarmSearch.loading?<p role="status">Searching saved groups…</p>:swarmSearch.error?<ErrorMessage message={swarmSearch.error}/>:<><p>{swarmResult?.total??0} matching saved groups · membership is not ownership.</p><div className="br-actions">{(swarmResult?.groups??[]).map((g:any)=><Button key={g.id} variant="soft" onClick={()=>onOpenSwarm(g.id)}>{g.name}<ArrowUpRight size={14}/></Button>)}</div>{swarmResult?.total>20&&<p>Showing the first 20. Refine your search to find more.</p>}</>}</section>}
<div hidden={!!selected||!!debounced&§ion!=='market'&§ion!=='holders'}>
{section==='fleet'&&<section className="br-fleet-summary"><div><h2>Shared Palantir roster</h2><p>{number(fleet.data?.registry?.count)} friendly identities · {number(fleetPlayers.filter(p=>p.relationship==='owned').length)} explicitly owned · {number(players.length)} saved for tracking</p><p className="br-muted">Local identities are friendly. To populate My bots, open a player and mark the relationship as owned. Starring only enables tracking.</p></div><button onClick={()=>setSettings(true)}>Manage relationships</button><ErrorMessage message={fleet.data?.sourceError??null}/>{fleet.data?.registry?.errors?.map((message:string)=><ErrorMessage key={message} message={message}/>)}<details><summary>Roster sources · {time(fleet.data?.registry?.refreshedAt)}</summary><p>{fleet.data?.registry?.sources?.join(' · ')}</p><p>{fleet.data?.coverage}</p></details></section>}
{(section==='market'||visited.has('market'))&&<div hidden={section!=='market'}><MarketBoard active={active&§ion==='market'&&!selected} query={section==='market'?debounced:''} searchValue={query} onSearch={setQuery} searchRef={exchangeSearchRef} onSelect={select} saved={saved} onWatch={toggle} busy={busy} onAsk={onAsk}/></div>}
{(section==='economy'||visited.has('economy'))&&<div hidden={section!=='economy'}><EconomyOverview onNavigate={onNavigate} active={active&§ion==='economy'&&!selected} onSelect={select} saved={saved} onWatch={toggle} busy={busy} onAsk={onAsk}/></div>}
{section==='opportunities'&&<OpportunityScan query={debounced} onSelect={select} saved={saved} onWatch={toggle} busy={busy} onAsk={onAsk}/>}
{section==='charts'&&<MarketDashboard items={activeItems} onSelect={select}/>}
{(section==='holders'||visited.has('holders'))&&<div hidden={section!=='holders'}><BankHoldings active={active&§ion==='holders'} revision={revision} searchRef={bankSearchRef} watchedItems={watches.data?.items??[]} saving={busy||watches.loading||!!watches.error} onSaveTarget={async(id,quantity,expected)=>{await action(async()=>{const current=await api('/watchlist');const item=current.items.find((i:any)=>i.itemId===id);if((item?.targetQuantity??null)!==expected){setRevision(x=>x+1);throw new Error('The shared target changed. Review its latest value before replacing it.')}if(!item||!item.active)await mutate('/items/watch','POST',{itemId:id});await mutate(`/items/${id}/target`,'PUT',{quantity});},'Shared accumulation target saved. Draft contributions remain local.')}} onSelect={select} onOpenWorld={onOpenWorld} onAsk={onAsk}/></div>}
{section==='production'&&<Production query={debounced}/>}
{section==='economics'&&<EconomicsResearch/>}
{section==='tools'&&<ResearchApiTools/>}
{section==='reports'&&<OriginalReport/>}
</div>
{(selected||(debounced&§ion!=='market'&§ion!=='holders')||section==='watchlists'||section==='fleet')&&<div className="br-split"><div className="br-list-pane" ref={listRef}><div className="br-list-head"><div className="br-tabs" role="tablist" aria-label="Search entity type"><button role="tab" aria-selected={tab==='item'} onClick={()=>setTab('item')}>Items <span>{debounced?search.data?.items?.length??0:activeItems.length}</span></button><button role="tab" aria-selected={tab==='player'} onClick={()=>setTab('player')}>Players <span>{section==='fleet'?fleetPlayers.length:debounced?search.data?.players?.length??0:players.length}</span></button></div>{isFleet&&<label className="br-filter">Roster<select value={fleetScope} onChange={e=>setFleetScope(e.target.value)}><option value="friendly">Palantir friendlies</option><option value="all">All known identities</option><option value="saved">Saved players only</option></select></label>}{tab==='player'&&<label className="br-filter">Relationship<select value={group} onChange={e=>setGroup(e.target.value)}><option value="all">All players</option>{Object.entries(relationships).map(([k,v])=><option key={k} value={k}>{v}{isFleet?` (${sourceRows.filter(e=>e.relationship===k&&(fleetScope==='all'||(fleetScope==='friendly'?e.friendly:e.saved))).length})`:''}</option>)}</select></label>}<div className="br-list-caption"><span>{isFleet?'Shared Palantir roster':debounced?'Search results':'Saved watchlist'}</span><span>{rows.length} {tab==='item'?'items':'players'}</span></div></div>{showWatchQuotes&&<WatchQuotesNotice state={watchQuotes}/>}<ErrorMessage message={listError}/>{debounced&&!isFleet&&search.data?.coverage&&<p className="br-coverage">{tab==='item'?search.data.coverage.items:search.data.coverage.players}</p>}{listLoading?<p className="br-loading" role="status">Loading {isFleet?'shared Palantir roster':debounced?'search results':'saved research'}…</p>:rows.length?<div className="br-entity-list">{rows.slice(0,visibleCount).map(e=><div key={keyOf(e)} className={`br-entity-row ${showWatchQuotes?'br-watch-entity-row':''} ${selected&&keyOf(e)===keyOf(selected)?'br-selected':''}`}><button className="br-entity-main" onClick={()=>select(e)} aria-pressed={selected?keyOf(e)===keyOf(selected):false}>{e.kind==='item'&&<ItemArt id={Number(e.key)} name={e.name}/>}<span><strong>{e.name}</strong><small>{e.kind==='item'?`Item #${e.key}`:relationships[e.relationship??'unclassified']}{isFleet&&e.teamNames?.length?` · ${e.teamNames.join(', ')}`:''}</small>{isFleet&&<small>{e.friendly?'Palantir friendly · ':''}{saved(e)?'Tracking saved':'Not tracked'}</small>}</span><ChevronRight size={18}/></button>{showWatchQuotes&&<WatchQuoteSummary row={watchQuotes.rows.get(Number(e.key))} state={watchQuotes}/>}<button className={`br-star ${saved(e)?'br-star-saved':''}`} aria-label={`${saved(e)?'Unsave':'Save'} ${e.name}`} aria-pressed={saved(e)} disabled={busy} onClick={()=>void toggle(e)}><Star size={19} fill={saved(e)?'currentColor':'none'}/></button></div>)}{rows.length>visibleCount&&<button className="br-load-more" onClick={()=>setVisibleCount(n=>n+60)}>Show more · {rows.length-visibleCount} remaining</button>}</div>:isFleet?<Empty title={group==='owned'?'No owned bots in this filter':'No players match these filters'}>{group==='owned'?'Apply a manual ownership correction in a player dossier. The friendly registry includes allies and does not automatically assign ownership.':'Try All known identities or clear your search and relationship filters.'}<button onClick={()=>{setFleetScope('all');setGroup('all');setQuery('')}}>Show all known identities</button><button onClick={()=>navigateSection('settings')}>Connect RS-SDK folder</button></Empty>:<Empty title={debounced?'No matching results':tab==='item'?'Start with an item you care about':'Keep an eye on your players'}>{debounced?'Try an exact item ID or player name. Search coverage is limited to available sources.':tab==='item'?'Search for steel bars, cannonballs or a rare item, then star it to start tracking.':'Search an exact player name, then save it. Saving a player never assigns ownership.'}</Empty>}</div><aside className="br-detail-pane" ref={detailRef} tabIndex={-1} aria-label="Selected entity details">{selected?<><button className="br-back" onClick={()=>{if(history.state?.blackrockEntity)history.back();else back()}}><ArrowLeft size={18}/>Back to results</button><nav className="br-research-trail" aria-label="Research trail"><button onClick={back}>Back to results</button>{trail.map((e,i)=><button key={`${keyOf(e)}-${i}`} onClick={()=>{setSelected(e);setTrail(trail.slice(0,i));}}><ChevronRight size={13}/>{e.name}</button>)}<span><ChevronRight size={13}/>{selected.name}</span></nav><Detail key={keyOf(selected)} entity={selected} revision={revision} saved={saved(selected)} savedNote={selected.kind==='player'?players.find((p:any)=>p.name.toLowerCase()===selected.name.toLowerCase())?.note??'':''} busy={busy} onSave={()=>void toggle(selected)} onOpenWorld={onOpenWorld} onSelect={select} onAsk={onAsk} onTabChange={view=>setSelected(old=>old?{...old,view}:old)} action={action}/></>:<div className="br-inspector-empty"><span className="br-eyebrow">RESEARCH INSPECTOR</span><h2>{section==='fleet'?'Select a player':'Select an item to research'}</h2><p>{section==='fleet'?'Select a player for published skills, observed holdings, ownership corrections and a link to their Palantir map view.':'Select an item to compare its market, observed holders, production costs and time value.'}</p><ul><li>Save items and set accumulation targets.</li><li>Keep your bots separate from allies and other players.</li><li>Compare source timestamps before acting.</li></ul><p className="br-muted">Public bank previews are partial. GP inflation and future appreciation remain research hypotheses.</p></div>}</aside></div>}
</div></div>
}
function Detail({entity,revision,saved,savedNote,busy,onSave,onOpenWorld,onSelect,onAsk,onTabChange,action}:{entity:Entity;revision:number;saved:boolean;savedNote:string;busy:boolean;onSave:()=>void;onOpenWorld:(name?:string)=>void;onSelect:(e:Entity)=>void;onAsk:(prompt:string)=>void;onTabChange:(view:Entity['view'])=>void;action:(fn:()=>Promise<unknown>,message:string)=>Promise<void>}){
const [tab,setTab]=useState<string>(entity.view??'overview'),[target,setTarget]=useState(''),[note,setNote]=useState(savedNote);const item=entity.kind==='item';
const resource=useResource(item?`/items/${entity.key}`:`/players?name=${encodeURIComponent(entity.name)}`,revision,30000);const data=resource.data;const quote=quoteRecord(data?.quote?.data);const holders=data?.holders??data?.holdings;const rows=holders?.data?.rows??[];
useEffect(()=>setTab(entity.view??'overview'),[entity.view]);
useEffect(()=>setNote(savedNote),[savedNote]);
useEffect(()=>{setTarget(data?.target?.targetQuantity==null?'':String(data.target.targetQuantity))},[data?.target?.targetQuantity]);
const refresh=()=>action(()=>mutate('/refresh','POST',{kind:entity.kind,key:entity.key}),'Refresh queued. Cached source timestamps remain unchanged until a new fetch succeeds.');
return <><div className="br-detail-heading">{item&&<ItemArt id={Number(entity.key)} name={data?.name??entity.name} size={48}/>}<div><span className="br-eyebrow">{item?`ITEM #${entity.key}`:'PLAYER DOSSIER'}</span><h2>{data?.name??entity.name}</h2></div><button className={`br-star ${saved?'br-star-saved':''}`} aria-label={`${saved?'Unsave':'Save'} ${entity.name}`} aria-pressed={saved} disabled={busy} onClick={onSave}><Star size={20} fill={saved?'currentColor':'none'}/></button></div><div className="br-detail-actions"><span className="br-muted">{saved?'Saved · collection enabled when service runs':'Not saved · on-demand research'}</span><button disabled={busy||!saved} title={saved?'Refresh saved research':'Save this item/player to refresh and track'} onClick={()=>void refresh()}><RefreshCw size={16}/>Refresh</button>{!item&&<button onClick={()=>onOpenWorld(entity.name)}><ArrowUpRight size={16}/>Open in World</button>}</div><div className="br-tabs br-detail-tabs" role="tablist" aria-label="Research view">{['overview',...(item?['orders','charts']:[]),'holders',...(item?['production']:[]),'history'].map(t=><button key={t} role="tab" aria-selected={tab===t} onClick={()=>{setTab(t);onTabChange(t as Entity['view'])}}>{t==='holders'?(item?'Holders':'Observed holdings'):t[0].toUpperCase()+t.slice(1)}</button>)}</div><div className="br-item-research-actions"><Button variant="soft" onClick={()=>onAsk(`Research ${entity.name} (${entity.kind} ${entity.key}). Current tab: ${tab}. Quote observation: ${time(data?.quote?.fetchedAt)}. Public bank previews are partial. Compare evidence, timestamps and liquidity before drawing conclusions.`)}>Ask about {entity.name}</Button>{item&&<Button variant="ghost" onClick={()=>onAsk(`Compare ${entity.name} (item ${entity.key}) with alternatives using current quotes, executed volume, production costs and observation coverage. Ask me which comparison items if needed.`)}>Compare with assistant</Button>}</div><SourcePayload data={data} name={`item-player-${entity.key}`}/><ErrorMessage message={resource.error}/>{resource.loading&&<p role="status">Loading public research…</p>}{tab==='charts'&&item?<MarketCharts itemId={Number(entity.key)} name={entity.name} compact/>:tab==='history'?<History kind={entity.kind} entityKey={entity.key}/>:tab==='production'?<Production query={entity.name} itemId={Number(entity.key)}/>:<>
{(tab==='overview'||tab==='orders')&&item&&<><section className="br-section"><h3>Current market</h3><SourceState value={data?.quote} label="Market quote"/><Facts values={[["Best buy bid · GP",quote?.bid],["Best sell ask · GP",quote?.ask],["Buy quantity · all prices",quote?.buyQuantity],["Sell quantity · all prices",quote?.sellQuantity],["7-day VWAP · GP",quote?.vwap],["7-day executed volume",quote?.volume],["Last trade · GP",quote?.lastPrice],["Last trade time",quote?.lastTradeAt?time(quote.lastTradeAt):null]]}/><p className="br-muted">Aggregate quantities span all prices. They are not executable depth at the best quote.</p></section><section className="br-section"><h3>Accumulation target</h3>{data?.target?.targetQuantity!=null?<><p className="br-target-total"><strong>{data.target.observedQuantity==null?'Not observed':`At least ${number(data.target.observedQuantity)}`}</strong><span> / {number(data.target.targetQuantity)} target</span></p><p className="br-warning">{data.target.coverage==='complete'?'Complete coverage':'Partial coverage — no exact purchase deficit'}{data.target.stale?' · Stale':''}</p><p className="br-muted">Owned quantities only. Observed {time(data.target.asOf)}.</p></>:<p className="br-muted">Set an optional quantity goal across My bots. Incomplete holdings never become an exact buying recommendation.</p>}<form className="br-inline" onSubmit={e=>{e.preventDefault();const quantity=Number(target);if(!Number.isSafeInteger(quantity)||quantity<1)return;void action(async()=>{if(!saved)await mutate('/items/watch','POST',{itemId:Number(entity.key)});return mutate(`/items/${entity.key}/target`,'PUT',{quantity});},'Accumulation target saved.')}}><label>Target quantity<input type="number" min="1" max={Number.MAX_SAFE_INTEGER} step="1" value={target} onChange={e=>setTarget(e.target.value)} placeholder="e.g. 100000" required/></label><button className="br-primary" disabled={busy||!target} type="submit">Save target</button><button type="button" disabled={busy||data?.target?.targetQuantity==null} onClick={()=>void action(()=>mutate(`/items/${entity.key}/target`,'PUT',{quantity:null}),'Target cleared. Watch and history are preserved.')}>Clear</button></form></section></>}
{tab==='overview'&&!item&&<section className="br-section"><h3>Ownership & provenance</h3><p className="br-relationship">{relationships[data?.relationship as Relationship]??'Unclassified'}{data?.conflict?' · Assignment conflict':''}</p><p className="br-muted">{data?.provenance?typeof data.provenance==='string'?data.provenance:Object.entries(data.provenance).filter(([,v])=>typeof v==='string').map(([k,v])=>`${k}: ${v}`).join(' · '):'No current assignment evidence.'}</p><label>Manual relationship correction<select disabled={busy} value={data?.relationship??'unclassified'} onChange={e=>void action(()=>mutate('/ownership','PUT',{name:entity.name,relationship:e.target.value}),'Manual correction saved. Historical membership remains as observed.')} >{Object.entries(relationships).map(([k,v])=><option key={k} value={k}>{v}</option>)}</select></label><button disabled={busy} onClick={()=>void action(()=>mutate(`/ownership?name=${encodeURIComponent(entity.name)}`,'DELETE'),'Correction reset to imported assignment or unclassified.')}>Reset to imported assignment</button><p className="br-muted">Manual corrections override team assignments until reset. A watched player is not automatically one of your bots.</p><label>Watch note<textarea maxLength={1000} value={note} onChange={e=>setNote(e.target.value)} placeholder="Purpose, observations, or context"/></label><button disabled={busy} onClick={()=>void action(()=>mutate('/api/watches','POST',{name:entity.name,note}),'Player watch note saved.')}>Save note & watch</button>{data?.dossier&&<details><summary>Public world dossier</summary><pre>{JSON.stringify(data.dossier,null,2)}</pre></details>}</section>}
{tab==='overview'&&!item&&<section className="br-section"><h3>Published skills</h3><SourceState value={data?.profile} label="Public skill profile"/>{data?.profile?.data?.rows?.length>0?<div className="br-scroll-table"><table><thead><tr><th>Skill</th><th>Level</th><th>Rank</th><th>Published time</th></tr></thead><tbody>{data.profile.data.rows.map((r:any)=><tr key={r.skill}><td>{r.skill}</td><td>{number(r.level)}</td><td>{number(r.rank)}</td><td>{r.time??'Unknown'}</td></tr>)}</tbody></table></div>:<p className="br-muted">No skill profile observed yet. This is not proof that the account does not exist.</p>}</section>}
{(tab==='holders'||tab==='overview')&&<section className="br-section"><h3>{item?'Observed holders':'Observed holdings'}</h3><SourceState value={holders} label="Public bank preview"/>{item&&<HolderSummary rows={rows} itemId={Number(entity.key)}/>}<p className="br-muted">Up to 50 accounts and 6 item previews per account. Bank save time may be unknown. Definition value is not GE liquidation value.</p>{rows.length?<div className="br-holder-list">{rows.map((r:any,i:number)=>{const name=r.player??r.name??entity.name;const items=r.items??[];const matching=item?items.filter((v:any)=>Number(v.id??v.itemId)===Number(entity.key)):items;return <div key={`${name}-${i}`} className="br-holder-row"><div><button className="br-text-button" onClick={()=>onSelect({kind:'player',key:name,name,relationship:r.relationship})}>{name}<ArrowUpRight size={14}/></button><small>{relationships[r.relationship as Relationship]??'Unclassified'}</small><button className="br-holder-map" onClick={()=>onOpenWorld(name)}><ArrowUpRight size={13}/>Locate in Palantir</button></div><div>{matching.length?matching.map((v:any)=><button key={v.id??v.itemId} className="br-holding-item" onClick={()=>onSelect({kind:'item',key:String(v.id??v.itemId),itemId:Number(v.id??v.itemId),name:v.name??v.label??`Item ${v.id??v.itemId}`})}><ItemArt id={Number(v.id??v.itemId)} name={v.name??v.label??'Item'} size={32}/><span>{v.name??v.label??`Item ${v.id??v.itemId}`}<strong>{number(v.quantity??v.count)}</strong></span></button>):<span className="br-muted">Selected item not observed in this preview</span>}</div></div>})}</div>:<Empty title="Holdings not observed">No matching preview is available. This does not establish zero holdings.</Empty>}</section>}
</> }</>
}
export default BlackrockWorkspace;