src/blackrock/ResearchTools.tsx
import {ItemSearchInput} from './ItemSearchInput';
import {useState} from 'react';
import {Button,Card} from '@whop/react/components';
import {ArrowUpRight,Download,Activity,ChartNoAxesCombined,Star,Users,Hammer,BookOpen,PanelLeft} from 'lucide-react';
import {api,number,time,download,type Entity} from './api';
import {ErrorMessage,Empty,Facts,TimeValue} from './Research';
import {ItemArt} from './ItemArt';
import {researchGroups,researchGroup,researchSection,type ResearchSection} from './navigation';
import './research-tools.css';
export function ResearchNavigation({section,onNavigate}:{section:ResearchSection;onNavigate:(s:ResearchSection)=>void}){
const [collapsed,setCollapsed]=useState(()=>{try{return localStorage.getItem('rs-blackrock:nav-collapsed')==='true'}catch{return false}});
const group=researchGroup(section);
const icons={economy:Activity,market:ChartNoAxesCombined,watchlists:Star,holders:Users,fleet:Users,production:Hammer,reports:BookOpen};
return <aside className={`br-flow-nav ${collapsed?'is-collapsed':''}`} aria-label="Blackrock navigation">
<Button variant="ghost" color="gray" className="br-flow-collapse" aria-label={collapsed?'Expand navigation':'Collapse navigation'} aria-expanded={!collapsed} onClick={()=>{setCollapsed(!collapsed);try{localStorage.setItem('rs-blackrock:nav-collapsed',String(!collapsed))}catch{}}}><PanelLeft size={18}/><span>Workspace</span></Button>
<nav aria-label="Blackrock research tools">{researchGroups.map(g=>{const Icon=icons[g.key];return <Button key={g.key} title={g.label} aria-label={g.label} variant={g===group?'soft':'ghost'} color={g===group?'blue':'gray'} aria-current={g===group?'page':undefined} onClick={()=>onNavigate(g.key)}><Icon size={18}/><span>{g.label}</span></Button>})}</nav>
<div className="br-flow-nav-note"><span>RS-BLACKROCK</span><p>Follow the evidence.<br/>Know the economy.</p></div>
</aside>;
}
export function ResearchSubnavigation({section,onNavigate}:{section:ResearchSection;onNavigate:(s:ResearchSection)=>void}){
const group=researchGroup(section);
return group.sections.length>1?<nav className="br-flow-subnav" aria-label={`${group.label} tools`}>{group.sections.map(key=><Button key={key} variant={section===key?'soft':'ghost'} color="gray" aria-current={section===key?'page':undefined} onClick={()=>onNavigate(key)}>{researchSection(key).label}</Button>)}</nav>:null;
}
export function SourcePayload({data,name='market-data'}:{data:any;name?:string}){return data?<details className="br-source-payload"><summary>Original API data & sources</summary><Button variant="soft" color="gray" onClick={()=>download(`${name}.json`,data)}><Download size={14}/>Download JSON</Button><pre>{JSON.stringify(data,null,2)}</pre></details>:null;}
export function EconomicsResearch(){
const [item,setItem]=useState('451'),[output,setOutput]=useState('2353'),[seconds,setSeconds]=useState(''),[gp,setGp]=useState(''),[data,setData]=useState<any>(null),[busy,setBusy]=useState(false),[error,setError]=useState<string|null>(null);
const id=Number(item),valid=Number.isSafeInteger(id)&&id>0&&id<=1000000,c=data?.conversion;
return <div className="br-economics-grid"><Card className="br-tool-surface"><h2>Buy time, or farm it?</h2><p>Use an item ID from Exchange or search. Runite ore is #451; its respawn delay is not the same as measured time per banked ore.</p><label>Acquisition item ID<ItemSearchInput label="Acquisition item ID" value={item} onChange={setItem} onPick={selected=>setItem(String(selected.id))}/></label>{valid?<TimeValue key={id} itemId={id}/>:<p>Enter a valid item ID.</p>}</Card><Card className="br-tool-surface"><h2>Ore → bar conversion</h2><p>Original smelting calculator: input asks, output bid, 5% seller tax and optional displaced farming income.</p><form onSubmit={async e=>{e.preventDefault();setBusy(true);setError(null);setData(null);try{const p=new URLSearchParams({output_id:output});if(seconds)p.set('seconds_per_bar',seconds);if(gp)p.set('gp_per_hour',gp);setData(await api('/research/conversion?'+p))}catch(e){setError((e as Error).message)}finally{setBusy(false)}}}><div className="br-form-grid"><label>Smelted output<select value={output} onChange={e=>{setOutput(e.target.value);setData(null)}}>{[[2349,'Bronze bar'],[2351,'Iron bar'],[2353,'Steel bar'],[2355,'Silver bar'],[2357,'Gold bar'],[2359,'Mithril bar'],[2361,'Adamantite bar'],[2363,'Runite bar']].map(([id,name])=><option key={id} value={id}>{name}</option>)}</select></label><label>Seconds per successful bar<input type="number" min="0.001" max="86400" step="any" value={seconds} onChange={e=>setSeconds(e.target.value)} placeholder="Optional · include banking"/></label><label>Alternative net GP / hour<input type="number" min="0" max="1000000000000" value={gp} onChange={e=>setGp(e.target.value)} placeholder="Optional · requires time"/></label></div><Button type="submit" disabled={busy||!!gp&&!seconds}>{busy?'Comparing quotes…':'Calculate conversion'}</Button></form><ErrorMessage message={error}/>{c&&<><Facts values={[["Input cost / bar",c.inputCostPerBar],["Output bid",c.outputBid],["Seller net / bar",c.sellerNetPerBar],["Contribution before time",c.contributionBeforeTimeGp],["Time cost / bar",c.timeCostPerBarGp],["Contribution after time",c.contributionAfterTimeGp]]}/><p className="br-warning">{c.caveat}</p><SourcePayload data={data} name="conversion"/></>}</Card></div>;
}
const tools=[
['Exchange overview','/research/overview?sort=buyValue&days=1&offset=0','marketOverview','research overview --sort buyValue --days 1'],
['Item lookup','/research/lookup?query=steel&days=7','market_lookup','search steel'],
['Current item quote','/research/quote?identifier=2353&days=7','market_quote','research quote --identifier 2353 --days 7'],
['Completed trade history','/research/history?identifier=2353&days=7','market_history','research history --identifier 2353 --days 7'],
['Buying demand','/buy-demand?itemId=1629&days=1','market_buy_demand','buy-demand 1629 --days 1'],
['Opportunity screens','/research/opportunities?top=5','market_opportunities','research opportunities --top 5'],
['Public bank holders','/research/bank?query=cannonball','market_bank','research bank --query cannonball'],
['Recipe catalog','/research/recipes?query=','market_recipes','research recipes'],
['Production economics','/research/production?recipe=smelt%3Acannonballs','market_production','research production --recipe smelt:cannonballs'],
['Smelting conversion','/research/conversion?output_id=2353','market_conversion','research conversion --output-id 2353'],
['Acquisition time','/research/time-value?identifier=451&seconds_per_item=60&gp_per_hour=100000','market_time_value','research time-value --identifier 451 --seconds-per-item 60 --gp-per-hour 100000'],
] as const;
export function ResearchApiTools(){return <section className="br-section br-tool-surface"><h2>The original research tools</h2><p>These routes use the existing market engine. Open a read-only JSON response or use the CLI / MCP from the same local service. Example time inputs are scenarios, not measured farm rates.</p><p className="br-muted">Run CLI commands in apps/rs-palantir. Start the unified MCP with <code>pnpm run mcp</code>. Exchange overview is the api-anything operation marketOverview; the remaining names below are unified MCP tools.</p><div className="br-scroll-table"><table><caption>Market API directory</caption><thead><tr><th>Tool</th><th>Original JSON response</th><th>MCP / learned operation</th><th>CLI</th></tr></thead><tbody>{tools.map(([label,path,mcp,cli])=><tr key={path}><td>{label}</td><td><a href={'/api/blackrock/v1'+path} target="_blank" rel="noreferrer">{path}</a></td><td><code>{mcp}</code></td><td><code>pnpm run blackrock -- {cli}</code></td></tr>)}</tbody></table></div><p>Saved items, targets, sampled observation history and collection status remain available alongside the original engine.</p><div className="br-actions"><a href="/api/blackrock/v1/watchlist/export" target="_blank" rel="noreferrer">Export saved watchlist</a><a href="/api/blackrock/v1/collector" target="_blank" rel="noreferrer">Collector status JSON</a><a href="/api/openapi.json" target="_blank" rel="noreferrer">API schema</a></div><p className="br-warning">Historical individual orders and buyer identities are not exposed. Bank snapshots are partial; no screen represents total game supply.</p></section>}
export function OriginalReport(){return <section className="br-section br-tool-surface"><h2>Local research reports</h2><p>Personal historical reports are not bundled. Start with Intelligence, Exchange or the assistant to build research from your own sources.</p><a href="#economy">Open economic intelligence</a></section>;}