File size: 4,495 Bytes
4fdaf19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import React, { useEffect, useState } from 'react';
import { Cpu, Eye, RefreshCw, Zap } from 'lucide-react';

/**
 * CPU / ZeroGPU deployment toggle (spec.md §4.5). Controls whether the engine
 * escalates to the OmniParser station (GPU path) for canvas/Figma surfaces.
 * CPU tier stays fully functional: DOM serializer + optical CVD/blur.
 */

interface Capabilities {
  perception_mode: string;
  visual_perception_enabled: boolean;
  perception_tier: string;
  omniparser_configured: boolean;
}

const PerceptionToggle: React.FC = () => {
  const [caps, setCaps] = useState<Capabilities | null>(null);
  const [busy, setBusy] = useState(false);
  const [note, setNote] = useState('');

  const load = () =>
    fetch('/api/account/capabilities')
      .then((response) => (response.ok ? response.json() : null))
      .then((body) => body?.data && setCaps(body.data))
      .catch(() => undefined);

  useEffect(() => {
    load();
  }, []);

  const setMode = async (mode: 'cpu' | 'zerogpu' | 'auto') => {
    setBusy(true);
    setNote('');
    try {
      const response = await fetch('/api/account/capabilities/perception', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ mode }),
      });
      if (response.status === 401) {
        setNote('Sign in with Hugging Face to change the deployment tier.');
      } else if (response.ok) {
        await load();
      } else {
        setNote(`Failed (${response.status})`);
      }
    } catch {
      setNote('API unreachable.');
    } finally {
      setBusy(false);
    }
  };

  const tier = caps?.perception_tier;

  return (
    <div className="space-y-4 pt-4">
      <h3 className="flex items-center gap-2 border-b border-gray-900 pb-2 text-xs font-bold uppercase tracking-wider text-teal-400">
        <Eye size={14} /> Perception tier — CPU / ZeroGPU deployment
      </h3>
      <p className="text-[11px] leading-relaxed text-gray-500">
        CPU tier runs the DOM serializer with optical color-blindness and acuity preprocessing —
        fully functional, no GPU. ZeroGPU tier additionally escalates to the OmniParser station for
        canvas and Figma-prototype surfaces the DOM can't describe.
      </p>

      <div className="grid grid-cols-3 gap-2">
        {([
          { id: 'cpu', label: 'CPU only', icon: Cpu, desc: 'DOM + optical filter' },
          { id: 'zerogpu', label: 'ZeroGPU', icon: Zap, desc: 'OmniParser visual escalation' },
          { id: 'auto', label: 'Auto', icon: Eye, desc: 'ZeroGPU if parser URL set' },
        ] as const).map((option) => {
          const active = caps?.perception_mode === option.id;
          return (
            <button
              key={option.id}
              onClick={() => setMode(option.id)}
              disabled={busy}
              className={`flex flex-col items-start gap-1 rounded-xl border p-3 text-left transition ${
                active
                  ? 'border-teal-500/50 bg-teal-500/10 text-teal-200'
                  : 'border-gray-800 bg-black/40 text-gray-400 hover:border-gray-700'
              }`}
            >
              <option.icon size={16} />
              <span className="text-xs font-bold">{option.label}</span>
              <span className="text-[9px] opacity-60">{option.desc}</span>
            </button>
          );
        })}
      </div>

      <div className="flex items-center justify-between rounded-xl border border-gray-800 bg-gray-900/40 p-3">
        <span className="text-[11px] text-gray-400">
          Active tier:{' '}
          <span className={`font-bold ${tier === 'zerogpu' ? 'text-teal-300' : 'text-gray-300'}`}>
            {busy ? <RefreshCw size={11} className="inline animate-spin" /> : tier || '…'}
          </span>
          {caps && !caps.omniparser_configured && caps.perception_tier === 'zerogpu' && (
            <span className="ml-2 text-amber-400">⚠ OMNIPARSER_BASE_URL not set</span>
          )}
        </span>
        <span
          className={`h-3 w-9 rounded-full transition ${caps?.visual_perception_enabled ? 'bg-teal-500' : 'bg-gray-700'}`}
        >
          <span
            className={`block h-3 w-3 rounded-full bg-white transition-transform ${
              caps?.visual_perception_enabled ? 'translate-x-6' : ''
            }`}
          />
        </span>
      </div>
      {note && <p className="text-[10px] text-amber-400">{note}</p>}
    </div>
  );
};

export default PerceptionToggle;