File size: 6,987 Bytes
80d8c84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import { useEffect, useState } from 'react';
import { Play, RotateCcw, Dices } from 'lucide-react';
import type { BackendRuntimeStatus, Difficulty, ScenarioTemplate, ResetParams } from '@/types';
import { cn } from '@/lib/utils';
import { sfx } from '@/lib/audio';
import { healthCheck } from '@/lib/api';

const TEMPLATES: { value: ScenarioTemplate; label: string }[] = [
  { value: 'math_reasoning', label: 'Math Reasoning' },
  { value: 'ml_benchmark', label: 'ML Benchmark' },
  { value: 'finance_trading', label: 'Finance Trading' },
];

const DIFFICULTIES: { value: Difficulty; label: string }[] = [
  { value: 'easy', label: 'Easy' },
  { value: 'medium', label: 'Medium' },
  { value: 'hard', label: 'Hard' },
];

interface ControlsProps {
  onStart: (params: ResetParams) => void;
  onStep?: () => void;
  disabled?: boolean;
  episodeActive?: boolean;
  className?: string;
  initialSeed?: number;
  initialTemplate?: ScenarioTemplate;
  initialDifficulty?: Difficulty;
  runtimeStatus?: BackendRuntimeStatus | null;
}

export default function Controls({
  onStart,
  onStep,
  disabled,
  episodeActive,
  className,
  initialSeed,
  initialTemplate,
  initialDifficulty,
  runtimeStatus,
}: ControlsProps) {
  const [seed, setSeed] = useState<string>(initialSeed?.toString() ?? '42');
  const [template, setTemplate] = useState<ScenarioTemplate>(initialTemplate ?? 'ml_benchmark');
  const [difficulty, setDifficulty] = useState<Difficulty>(initialDifficulty ?? 'medium');
  const [backendStatus, setBackendStatus] = useState<'checking' | 'ok' | 'error'>('checking');
  const [backendMessage, setBackendMessage] = useState<string>('Checking backend connection...');

  useEffect(() => {
    if (initialSeed !== undefined) {
      setSeed(initialSeed.toString());
    }
  }, [initialSeed]);

  useEffect(() => {
    if (initialTemplate) {
      setTemplate(initialTemplate);
    }
  }, [initialTemplate]);

  useEffect(() => {
    if (initialDifficulty) {
      setDifficulty(initialDifficulty);
    }
  }, [initialDifficulty]);

  useEffect(() => {
    let cancelled = false;

    async function checkBackend() {
      setBackendStatus('checking');
      setBackendMessage('Checking backend connection...');
      try {
        await healthCheck();
        if (!cancelled) {
          setBackendStatus('ok');
          setBackendMessage('Backend connected. The live environment is ready.');
        }
      } catch (error) {
        if (!cancelled) {
          const message = error instanceof Error ? error.message : 'Backend unavailable.';
          setBackendStatus('error');
          setBackendMessage(message);
        }
      }
    }

    void checkBackend();
    return () => {
      cancelled = true;
    };
  }, []);

  function randomSeed() { sfx.click(); setSeed(Math.floor(Math.random() * 10000).toString()); }
  function handleStart() { sfx.click(); onStart({ seed: seed ? parseInt(seed, 10) : undefined, template, difficulty }); }

  return (
    <div className={cn('rounded-lg border border-border bg-card p-4', className)}>
      <div className="mb-3">
        <h2 className="text-sm font-semibold">Replication Setup</h2>
        <p className="mt-1 text-xs text-muted-foreground">
          Choose the seeded paper-derived benchmark that becomes this negotiation environment.
        </p>
        <p
          className={cn(
            'mt-2 text-xs',
            backendStatus === 'ok' && 'text-emerald-600',
            backendStatus === 'checking' && 'text-muted-foreground',
            backendStatus === 'error' && 'text-destructive',
          )}
        >
          {backendMessage}
        </p>
        {runtimeStatus && (
          <p className="mt-1 text-xs text-muted-foreground">
            Scientist runtime: <span className="font-medium text-foreground">{runtimeStatus.scientist_runtime}</span>
            {' '}({runtimeStatus.scientist_model})
          </p>
        )}
      </div>
      <div className="space-y-3">
        <div>
          <label className="mb-1 block text-xs font-medium text-muted-foreground">Scenario Family</label>
          <select value={template} onChange={(e) => setTemplate(e.target.value as ScenarioTemplate)} disabled={disabled || episodeActive}
            className="w-full rounded-md border border-border bg-background px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50">
            {TEMPLATES.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
          </select>
        </div>
        <div>
          <label className="mb-1 block text-xs font-medium text-muted-foreground">Difficulty</label>
          <div className="flex gap-1">
            {DIFFICULTIES.map((d) => (
              <button key={d.value} onClick={() => { sfx.click(); setDifficulty(d.value); }} disabled={disabled || episodeActive}
                className={cn('flex-1 rounded-md border px-2 py-1.5 text-xs font-medium transition-colors',
                  difficulty === d.value ? 'border-primary bg-primary/10 text-primary' : 'border-border text-muted-foreground hover:bg-muted disabled:opacity-50')}>
                {d.label}
              </button>
            ))}
          </div>
        </div>
        <div>
          <label className="mb-1 block text-xs font-medium text-muted-foreground">Seeded Benchmark</label>
          <div className="flex gap-1.5">
            <input type="number" value={seed} onChange={(e) => setSeed(e.target.value)} disabled={disabled || episodeActive} placeholder="Random"
              className="w-full rounded-md border border-border bg-background px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50" />
            <button onClick={randomSeed} disabled={disabled || episodeActive}
              className="rounded-md border border-border p-1.5 text-muted-foreground hover:bg-muted disabled:opacity-50" title="Random seed">
              <Dices className="h-4 w-4" />
            </button>
          </div>
        </div>
        <div className="flex gap-2 pt-1">
          <button onClick={handleStart} disabled={disabled}
            className={cn('flex flex-1 items-center justify-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium transition-colors disabled:opacity-50',
              episodeActive ? 'border border-border text-muted-foreground hover:bg-muted' : 'bg-primary text-primary-foreground hover:bg-primary/90')}>
            {episodeActive ? (<><RotateCcw className="h-4 w-4" />Reset Episode</>) : (<><Play className="h-4 w-4" />Start Replication</>)}
          </button>
          {episodeActive && onStep && (
            <button onClick={onStep} disabled={disabled}
              className="flex items-center gap-1.5 rounded-md bg-scientist px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-scientist/90 disabled:opacity-50">
              <Play className="h-4 w-4" />Advance Round
            </button>
          )}
        </div>
      </div>
    </div>
  );
}