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,
};
}
|