landscapeforge / frontend /src /pages /RunWithLlm.jsx
mnawfal29's picture
Upload folder using huggingface_hub
4a535ea verified
import { useState, useRef } from 'react'
import { Play, Terminal, Activity } from 'lucide-react'
import { llmRunStream } from '../lib/api.js'
import { RewardBreakdown } from '../components/RewardBreakdown.jsx'
import { TurnCard } from '../components/TurnCard.jsx'
import { KpiCard } from '../components/KpiCard.jsx'
const PRESET_ENDPOINTS = [
{ label: 'Ollama (localhost:11434)', url: 'http://localhost:11434/v1' },
{ label: 'Hugging Face Router', url: 'https://router.huggingface.co/v1' },
{ label: 'OpenAI', url: 'https://api.openai.com/v1' },
{ label: 'Custom', url: '' },
]
const MODELS = [
'qwen2.5:3b', 'qwen2.5:7b', 'qwen2.5:1.5b',
'Qwen/Qwen2.5-7B-Instruct', 'Qwen/Qwen2.5-3B-Instruct',
'meta-llama/Llama-3.2-3B-Instruct', 'gpt-4o-mini',
]
export function RunWithLlm() {
const [endpoint, setEndpoint] = useState(PRESET_ENDPOINTS[0].label)
const [customUrl, setCustomUrl] = useState('')
const [apiKey, setApiKey] = useState('')
const [model, setModel] = useState(MODELS[0])
const [tier, setTier] = useState('T0')
const [seed, setSeed] = useState(42)
const [temperature, setTemperature] = useState(0.7)
const [maxTurns, setMaxTurns] = useState(10)
const [running, setRunning] = useState(false)
const [turns, setTurns] = useState([])
const [header, setHeader] = useState(null)
const [done, setDone] = useState(null)
const stopRef = useRef(null)
function run() {
setTurns([]); setHeader(null); setDone(null); setRunning(true)
const preset = PRESET_ENDPOINTS.find(p => p.label === endpoint)
const base_url = (customUrl || preset.url).replace(/\/$/, '')
if (stopRef.current) stopRef.current()
stopRef.current = llmRunStream(
{
base_url, api_key: apiKey, model,
tier, seed, temperature, max_turns: maxTurns,
},
(ev) => {
if (ev.kind === 'header') {
setHeader(ev)
} else if (ev.kind === 'turn') {
// SSE event uses `kind` for event type and `kind_of` for action kind;
// TurnCard expects `kind` = action kind, so rename here.
setTurns(prev => [...prev, { ...ev, kind: ev.kind_of }])
} else if (ev.kind === 'done') {
setDone(ev)
setRunning(false)
} else if (ev.kind === 'error') {
setHeader(h => ({ ...(h || {}), error: ev.message }))
setRunning(false)
}
},
)
}
return (
<div className="grid grid-cols-[1fr_320px] gap-5">
{/* ─── MAIN PANE ─── */}
<div className="space-y-5">
<div className="card">
<div className="flex items-center gap-2 mb-3">
<Terminal size={18} className="text-subtle" />
<h3>Transcript</h3>
</div>
{!header && !turns.length && (
<p className="text-muted italic text-sm">
Configure the LLM on the right and hit <strong>Run episode</strong>.
Each turn streams here as the model plays.
</p>
)}
{header && (
<div className="border-b border-border/50 pb-3 mb-3">
<div className="text-sm text-muted">
Model <span className="chip border-border text-ink">{header.model}</span>
&nbsp;via&nbsp;
<span className="chip border-border text-ink">{header.base_url}</span>
</div>
<div className="text-sm mt-2">
<strong>Landscape:</strong> {header.landscape}
</div>
<div className="text-sm text-muted mt-1">
Dim: <strong>{header.dim}</strong> · Initial budget:{' '}
<strong>{header.budget}</strong>
</div>
{header.error && (
<div className="mt-2 p-3 rounded border border-bad/40 bg-bad/10 text-bad">
{header.error}
</div>
)}
</div>
)}
<div className="space-y-3">
{turns.map((t, i) => <TurnCard key={i} {...t} />)}
</div>
</div>
{done && <EpisodeDone done={done} />}
</div>
{/* ─── SIDEBAR ─── */}
<aside className="card space-y-4 h-fit sticky top-4">
<div>
<h3 className="mb-1">Connect an LLM</h3>
<p className="text-xs text-muted">
Point at any OpenAI-compatible <code>/v1/chat/completions</code> endpoint.
</p>
</div>
<div>
<label className="label">Endpoint</label>
<select
className="input"
value={endpoint}
onChange={e => setEndpoint(e.target.value)}
>
{PRESET_ENDPOINTS.map(p => (
<option key={p.label} value={p.label}>{p.label}</option>
))}
</select>
</div>
<div>
<label className="label">Model</label>
<input
className="input font-mono text-xs"
list="model-list"
value={model}
onChange={e => setModel(e.target.value)}
/>
<datalist id="model-list">
{MODELS.map(m => <option key={m} value={m} />)}
</datalist>
</div>
<div>
<label className="label">Custom base URL (optional)</label>
<input
className="input font-mono text-xs"
placeholder="http://localhost:8080/v1"
value={customUrl}
onChange={e => setCustomUrl(e.target.value)}
/>
</div>
<div>
<label className="label">API key (optional)</label>
<input
type="password"
className="input font-mono text-xs"
placeholder="Bearer <key>"
value={apiKey}
onChange={e => setApiKey(e.target.value)}
/>
</div>
<hr className="border-border/50" />
<h3>Episode config</h3>
<div>
<label className="label">Tier</label>
<select className="input" value={tier} onChange={e => setTier(e.target.value)}>
<option>T0</option><option>T1</option><option>T2</option>
</select>
</div>
<RangeRow label={`Seed: ${seed}`} min={0} max={100} step={1}
value={seed} onChange={setSeed} />
<RangeRow label={`Temperature: ${temperature.toFixed(2)}`}
min={0} max={1.5} step={0.05}
value={temperature} onChange={setTemperature} />
<RangeRow label={`Max turns: ${maxTurns}`} min={3} max={15} step={1}
value={maxTurns} onChange={setMaxTurns} />
<button
className="btn-primary w-full py-3"
disabled={running}
onClick={run}
>
{running
? <><Activity size={16} className="animate-pulse" /> Running…</>
: <><Play size={16} /> Run episode</>}
</button>
</aside>
</div>
)
}
function RangeRow({ label, min, max, step, value, onChange }) {
return (
<div>
<label className="label">{label}</label>
<input
type="range"
min={min} max={max} step={step} value={value}
onChange={e => onChange(Number(e.target.value))}
className="w-full accent-accent"
/>
</div>
)
}
function EpisodeDone({ done }) {
const reward = done.reward
const speedup = done.speedup_vs_adam
const myProg = done.my_progress
const adamProg = done.adam_progress
const rewardTone =
reward >= 0.5 ? 'good' : reward >= 0 ? 'warn' : 'bad'
// --- Speedup display ---
// "Speedup" only makes sense when both optimizers descended. Handle the
// degenerate cases cleanly rather than showing "-0.44×" (mathematically
// correct, semantically nonsense).
let speedupDisplay, speedupTone, speedupSub
if (myProg < 0) {
// Our optimizer went uphill
speedupDisplay = 'diverged'
speedupTone = 'bad'
speedupSub = `f moved +${Math.abs(myProg).toFixed(2)} (wrong direction)`
} else if (adamProg <= 0) {
// Adam itself couldn't descend — unfair denominator
speedupDisplay = myProg > 0 ? '∞' : '—'
speedupTone = myProg > 0 ? 'good' : 'warn'
speedupSub = 'Adam made no progress on this landscape'
} else {
const f = speedup < 100 ? speedup.toFixed(2) : Math.round(speedup).toString()
speedupDisplay = `${f}×`
speedupTone = speedup >= 1.0 ? 'good' : 'warn'
speedupSub = `descent ${myProg.toFixed(2)} vs Adam ${adamProg.toFixed(2)}`
}
// --- Verdict ---
let verdict, verdictTone, verdictSub
if (myProg < 0) {
verdict = 'Diverged'
verdictTone = 'bad'
verdictSub = 'optimizer moved away from the minimum'
} else if (adamProg <= 0) {
verdict = myProg > 0 ? 'Succeeds where Adam fails' : 'Tied · both stuck'
verdictTone = myProg > 0 ? 'good' : 'warn'
verdictSub = `you: ${myProg.toFixed(2)}, Adam: ${adamProg.toFixed(2)}`
} else if (speedup >= 1.5) {
verdict = 'Beats Adam'
verdictTone = 'good'
verdictSub = `${((speedup - 1) * 100).toFixed(0)}% further than Adam`
} else if (speedup >= 1.1) {
verdict = 'Edges Adam'
verdictTone = 'good'
verdictSub = `${((speedup - 1) * 100).toFixed(0)}% further than Adam`
} else if (speedup >= 0.9) {
verdict = 'Matches Adam'
verdictTone = 'warn'
verdictSub = 'within ±10% of Adam'
} else {
verdict = 'Behind Adam'
verdictTone = 'bad'
verdictSub = `covered ${(speedup * 100).toFixed(0)}% of Adam's descent`
}
return (
<div className="card"
style={{ background: 'linear-gradient(180deg, rgba(226,135,99,0.07) 0%, rgba(42,40,36,0) 60%)' }}>
<div className="flex items-baseline gap-3 mb-4">
<span className="chip border-accent text-accent uppercase tracking-wider text-[0.7rem]">
Episode complete
</span>
<span className="text-subtle text-sm">
ended by <code className="text-muted">{done.reason}</code>
</span>
</div>
<div className="grid grid-cols-3 gap-3 mb-5">
<KpiCard
label="Reward"
value={reward.toFixed(3)}
sub="GRPO scalar · range [−1.5, +1.8]"
tone={rewardTone}
sign={reward >= 0 ? '+' : ''} />
<KpiCard
label="Speedup vs Adam"
value={speedupDisplay}
sub={speedupSub}
tone={speedupTone} />
<KpiCard
label="Verdict"
value={verdict}
sub={verdictSub}
tone={verdictTone} />
</div>
<RewardBreakdown breakdown={done.breakdown} total={reward} />
</div>
)
}