File size: 3,061 Bytes
99d1f6e
 
382b9aa
99d1f6e
2b62b0c
99d1f6e
34f01ec
 
 
99d1f6e
 
 
 
f90228b
99d1f6e
 
34f01ec
 
 
 
 
 
99d1f6e
 
 
34f01ec
99d1f6e
 
34f01ec
99d1f6e
 
 
 
34f01ec
99d1f6e
 
 
 
 
 
 
 
34f01ec
 
99d1f6e
34f01ec
99d1f6e
 
34f01ec
 
 
 
382b9aa
34f01ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382b9aa
34f01ec
 
382b9aa
 
 
34f01ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b62b0c
34f01ec
 
 
99d1f6e
 
 
 
34f01ec
 
99d1f6e
 
 
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
import { useState, useEffect, useCallback } from "react";
import { encryptText, decryptText } from "@/lib/crypto";
import { trackUmamiEvent, AnalyticsEvent } from "@/lib/umami";

const STORAGE_KEY = "pram_api_keys_v2";

export interface ApiKeyConfig {
  id: string;
  name: string;
  provider: string;
  baseUrl: string;
  apiKey: string;
  modelName: string;
  maxTokens?: number;
}

function generateId() {
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`;
}

export function useApiKeys() {
  const [configs, setConfigs] = useState<ApiKeyConfig[]>([]);
  const [isLoading, setIsLoading] = useState(true);

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

  const loadConfigs = async () => {
    try {
      const encrypted = localStorage.getItem(STORAGE_KEY);
      if (encrypted) {
        const decrypted = await decryptText(encrypted);
        setConfigs(JSON.parse(decrypted));
      }
    } catch {
      localStorage.removeItem(STORAGE_KEY);
    } finally {
      setIsLoading(false);
    }
  };

  const persist = useCallback(async (next: ApiKeyConfig[]) => {
    const encrypted = await encryptText(JSON.stringify(next));
    localStorage.setItem(STORAGE_KEY, encrypted);
    setConfigs(next);
  }, []);

  const addConfig = useCallback(
    async (config: Omit<ApiKeyConfig, "id">) => {
      const next = [...configs, { ...config, id: generateId() }];
      await persist(next);
      trackUmamiEvent(AnalyticsEvent.API_KEY_ADDED, { provider: config.provider });
      return next[next.length - 1].id;
    },
    [configs, persist],
  );

  const updateConfig = useCallback(
    async (id: string, updates: Partial<Omit<ApiKeyConfig, "id">>) => {
      const next = configs.map((c) => {
        if (c.id !== id) return c;
        // If apiKey is empty string, keep the old one (don't overwrite)
        const apiKey =
          updates.apiKey === "" || updates.apiKey === undefined
            ? c.apiKey
            : updates.apiKey;
        return { ...c, ...updates, apiKey };
      });
      await persist(next);
    },
    [configs, persist],
  );

  const removeConfig = useCallback(
    async (id: string) => {
      const target = configs.find((c) => c.id === id);
      const next = configs.filter((c) => c.id !== id);
      await persist(next);
      if (target) {
        trackUmamiEvent(AnalyticsEvent.API_KEY_REMOVED, { provider: target.provider });
      }
    },
    [configs, persist],
  );

  const getConfig = useCallback(
    (id: string) => configs.find((c) => c.id === id),
    [configs],
  );

  return {
    configs,
    isLoading,
    addConfig,
    updateConfig,
    removeConfig,
    getConfig,
    hasConfigs: configs.length > 0,
  };
}

// Backward-compatible hook for places that only need the first (default) key
export function useApiKey() {
  const { configs, isLoading, addConfig, removeConfig } =
    useApiKeys();

  const storedKey = configs[0] ?? null;

  return {
    storedKey,
    isLoading,
    saveKey: addConfig,
    removeKey: removeConfig,
    hasKey: !!storedKey,
  };
}