File size: 4,248 Bytes
dd87944
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import React, { useEffect, useState } from "react";
import { http } from "../lib/api";
import { ExternalLink, KeyRound, RefreshCw } from "lucide-react";

const KEY_LINKS = [
  { id: "openrouter", label: "OpenRouter", url: "https://openrouter.ai/keys", hint: "Modèles free (Llama, Gemma, Qwen)" },
  { id: "deepseek", label: "DeepSeek", url: "https://platform.deepseek.com/api_keys", hint: "Chat + R1 Reasoner" },
  { id: "groq", label: "Groq", url: "https://console.groq.com/keys", hint: "Llama / Gemma gratuit" },
  { id: "huggingface", label: "Hugging Face", url: "https://huggingface.co/settings/tokens", hint: "HF_TOKEN (Read)" },
  { id: "gemini", label: "Gemini", url: "https://aistudio.google.com/apikey", hint: "Google AI Studio" },
  { id: "openai", label: "OpenAI", url: "https://platform.openai.com/api-keys", hint: "GPT-4o mini" },
  { id: "anthropic", label: "Anthropic", url: "https://console.anthropic.com/settings/keys", hint: "Claude" },
];

export default function LLMKeysPanel() {
  const [status, setStatus] = useState(null);
  const [loading, setLoading] = useState(true);

  const refresh = async () => {
    setLoading(true);
    try {
      const r = await http.get("/llm/status");
      setStatus(r.data);
    } catch (_) {
      setStatus(null);
    } finally {
      setLoading(false);
    }
  };

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

  const live = status?.live || {};

  return (
    <div className="h-full overflow-y-auto scrollbar-thin p-4 space-y-4" data-testid="llm-keys-panel">
      <div className="flex items-center justify-between gap-2">
        <div className="flex items-center gap-2">
          <KeyRound size={16} style={{ color: "var(--mode-color)" }} />
          <h3 className="text-sm font-semibold">Clés IA (prod)</h3>
        </div>
        <button
          type="button"
          onClick={refresh}
          disabled={loading}
          className="p-1.5 rounded-lg em-hover text-muted-em"
          title="Actualiser"
        >
          <RefreshCw size={14} className={loading ? "animate-spin" : ""} />
        </button>
      </div>

      <p className="text-[11px] text-muted-em leading-relaxed">
        Configure localement avec{" "}
        <code className="text-[10px] px-1 rounded" style={{ background: "var(--emo-subtle-bg)" }}>scripts\setup-llm-keys.ps1</code>
        {" "}puis{" "}
        <code className="text-[10px] px-1 rounded" style={{ background: "var(--emo-subtle-bg)" }}>scripts\sync-hf-secrets.bat</code>
      </p>

      <div className="space-y-1.5">
        {KEY_LINKS.map((k) => {
          const configured = status?.[k.id];
          const row = live[k.id];
          const ok = configured && row?.ok === true;
          const bad = configured && row?.ok === false;
          const dot = !configured ? "emo-status-dot-offline" : ok ? "emo-status-dot-online" : bad ? "bg-red-500" : "bg-amber-500";
          return (
            <div
              key={k.id}
              className="flex items-center gap-2 px-2.5 py-2 rounded-xl text-xs"
              style={{ background: "var(--emo-subtle-bg)", border: "1px solid var(--emo-border)" }}
            >
              <div className={`w-2 h-2 rounded-full flex-shrink-0 ${dot}`} />
              <div className="flex-1 min-w-0">
                <p className="font-medium truncate">{k.label}</p>
                <p className="text-[10px] text-muted-em truncate">
                  {!configured ? "Clé absente sur HF" : bad ? (row.detail || "Erreur").slice(0, 60) : ok ? "Opérationnel" : "Clé présente"}
                </p>
              </div>
              <a
                href={k.url}
                target="_blank"
                rel="noopener noreferrer"
                className="p-1 rounded em-hover text-muted-em flex-shrink-0"
                title={k.hint}
              >
                <ExternalLink size={12} />
              </a>
            </div>
          );
        })}
      </div>

      <a
        href="https://huggingface.co/spaces/Xroxx/emo-online-api/settings"
        target="_blank"
        rel="noopener noreferrer"
        className="block text-center text-xs py-2.5 rounded-xl transition emo-btn-soft"
      >
        Ouvrir secrets HF Space →
      </a>
    </div>
  );
}