Added multi-api support and advanced mode
This commit is contained in:
@@ -17,6 +17,8 @@ const DEFAULT_SETTINGS = {
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
apiKeys: [],
|
apiKeys: [],
|
||||||
activeApiKeyId: "",
|
activeApiKeyId: "",
|
||||||
|
apiConfigs: [],
|
||||||
|
activeApiConfigId: "",
|
||||||
apiBaseUrl: "https://api.openai.com/v1",
|
apiBaseUrl: "https://api.openai.com/v1",
|
||||||
apiKeyHeader: "Authorization",
|
apiKeyHeader: "Authorization",
|
||||||
apiKeyPrefix: "Bearer ",
|
apiKeyPrefix: "Bearer ",
|
||||||
@@ -91,6 +93,99 @@ chrome.runtime.onInstalled.addListener(async () => {
|
|||||||
: `key-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
: `key-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||||
updates.apiKeys = [{ id, name: "Default", key: stored.apiKey }];
|
updates.apiKeys = [{ id, name: "Default", key: stored.apiKey }];
|
||||||
updates.activeApiKeyId = id;
|
updates.activeApiKeyId = id;
|
||||||
|
} else if (hasApiKeys && stored.activeApiKeyId) {
|
||||||
|
const exists = stored.apiKeys.some((key) => key.id === stored.activeApiKeyId);
|
||||||
|
if (!exists) {
|
||||||
|
updates.activeApiKeyId = stored.apiKeys[0].id;
|
||||||
|
}
|
||||||
|
} else if (hasApiKeys && !stored.activeApiKeyId) {
|
||||||
|
updates.activeApiKeyId = stored.apiKeys[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasApiConfigs =
|
||||||
|
Array.isArray(stored.apiConfigs) && stored.apiConfigs.length > 0;
|
||||||
|
|
||||||
|
if (!hasApiConfigs) {
|
||||||
|
const fallbackKeyId =
|
||||||
|
updates.activeApiKeyId ||
|
||||||
|
stored.activeApiKeyId ||
|
||||||
|
stored.apiKeys?.[0]?.id ||
|
||||||
|
"";
|
||||||
|
const id = crypto?.randomUUID
|
||||||
|
? crypto.randomUUID()
|
||||||
|
: `config-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||||
|
updates.apiConfigs = [
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
name: "Default",
|
||||||
|
apiBaseUrl: stored.apiBaseUrl || DEFAULT_SETTINGS.apiBaseUrl,
|
||||||
|
apiKeyHeader: stored.apiKeyHeader || DEFAULT_SETTINGS.apiKeyHeader,
|
||||||
|
apiKeyPrefix: stored.apiKeyPrefix || DEFAULT_SETTINGS.apiKeyPrefix,
|
||||||
|
model: stored.model || DEFAULT_SETTINGS.model,
|
||||||
|
apiKeyId: fallbackKeyId,
|
||||||
|
apiUrl: "",
|
||||||
|
requestTemplate: "",
|
||||||
|
advanced: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
updates.activeApiConfigId = id;
|
||||||
|
} else if (stored.activeApiConfigId) {
|
||||||
|
const exists = stored.apiConfigs.some(
|
||||||
|
(config) => config.id === stored.activeApiConfigId
|
||||||
|
);
|
||||||
|
if (!exists) {
|
||||||
|
updates.activeApiConfigId = stored.apiConfigs[0].id;
|
||||||
|
}
|
||||||
|
const fallbackKeyId =
|
||||||
|
updates.activeApiKeyId ||
|
||||||
|
stored.activeApiKeyId ||
|
||||||
|
stored.apiKeys?.[0]?.id ||
|
||||||
|
"";
|
||||||
|
const normalizedConfigs = stored.apiConfigs.map((config) => ({
|
||||||
|
...config,
|
||||||
|
apiKeyId: config.apiKeyId || fallbackKeyId,
|
||||||
|
apiUrl: config.apiUrl || "",
|
||||||
|
requestTemplate: config.requestTemplate || "",
|
||||||
|
advanced: Boolean(config.advanced)
|
||||||
|
}));
|
||||||
|
const needsUpdate = normalizedConfigs.some((config, index) => {
|
||||||
|
const original = stored.apiConfigs[index];
|
||||||
|
return (
|
||||||
|
config.apiKeyId !== original.apiKeyId ||
|
||||||
|
(config.apiUrl || "") !== (original.apiUrl || "") ||
|
||||||
|
(config.requestTemplate || "") !== (original.requestTemplate || "") ||
|
||||||
|
Boolean(config.advanced) !== Boolean(original.advanced)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (needsUpdate) {
|
||||||
|
updates.apiConfigs = normalizedConfigs;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updates.activeApiConfigId = stored.apiConfigs[0].id;
|
||||||
|
const fallbackKeyId =
|
||||||
|
updates.activeApiKeyId ||
|
||||||
|
stored.activeApiKeyId ||
|
||||||
|
stored.apiKeys?.[0]?.id ||
|
||||||
|
"";
|
||||||
|
const normalizedConfigs = stored.apiConfigs.map((config) => ({
|
||||||
|
...config,
|
||||||
|
apiKeyId: config.apiKeyId || fallbackKeyId,
|
||||||
|
apiUrl: config.apiUrl || "",
|
||||||
|
requestTemplate: config.requestTemplate || "",
|
||||||
|
advanced: Boolean(config.advanced)
|
||||||
|
}));
|
||||||
|
const needsUpdate = normalizedConfigs.some((config, index) => {
|
||||||
|
const original = stored.apiConfigs[index];
|
||||||
|
return (
|
||||||
|
config.apiKeyId !== original.apiKeyId ||
|
||||||
|
(config.apiUrl || "") !== (original.apiUrl || "") ||
|
||||||
|
(config.requestTemplate || "") !== (original.requestTemplate || "") ||
|
||||||
|
Boolean(config.advanced) !== Boolean(original.advanced)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (needsUpdate) {
|
||||||
|
updates.apiConfigs = normalizedConfigs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(updates).length) {
|
if (Object.keys(updates).length) {
|
||||||
@@ -188,6 +283,9 @@ async function handleAnalysisRequest(port, payload, signal) {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
apiKey,
|
apiKey,
|
||||||
|
apiMode,
|
||||||
|
apiUrl,
|
||||||
|
requestTemplate,
|
||||||
apiBaseUrl,
|
apiBaseUrl,
|
||||||
apiKeyHeader,
|
apiKeyHeader,
|
||||||
apiKeyPrefix,
|
apiKeyPrefix,
|
||||||
@@ -199,19 +297,31 @@ async function handleAnalysisRequest(port, payload, signal) {
|
|||||||
tabId
|
tabId
|
||||||
} = payload || {};
|
} = payload || {};
|
||||||
|
|
||||||
if (!apiBaseUrl) {
|
const isAdvanced = apiMode === "advanced";
|
||||||
safePost(port, { type: "ERROR", message: "Missing API base URL." });
|
if (isAdvanced) {
|
||||||
return;
|
if (!apiUrl) {
|
||||||
}
|
safePost(port, { type: "ERROR", message: "Missing API URL." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!requestTemplate) {
|
||||||
|
safePost(port, { type: "ERROR", message: "Missing request template." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!apiBaseUrl) {
|
||||||
|
safePost(port, { type: "ERROR", message: "Missing API base URL." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (apiKeyHeader && !apiKey) {
|
if (apiKeyHeader && !apiKey) {
|
||||||
safePost(port, { type: "ERROR", message: "Missing API key." });
|
safePost(port, { type: "ERROR", message: "Missing API key." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
safePost(port, { type: "ERROR", message: "Missing model name." });
|
safePost(port, { type: "ERROR", message: "Missing model name." });
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!postingText) {
|
if (!postingText) {
|
||||||
@@ -230,20 +340,35 @@ async function handleAnalysisRequest(port, payload, signal) {
|
|||||||
openKeepalive(tabId);
|
openKeepalive(tabId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await streamChatCompletion({
|
if (isAdvanced) {
|
||||||
apiKey,
|
await streamCustomCompletion({
|
||||||
apiBaseUrl,
|
apiKey,
|
||||||
apiKeyHeader,
|
apiUrl,
|
||||||
apiKeyPrefix,
|
requestTemplate,
|
||||||
model,
|
systemPrompt: systemPrompt || "",
|
||||||
systemPrompt: systemPrompt || "",
|
userMessage,
|
||||||
userMessage,
|
signal,
|
||||||
signal,
|
onDelta: (text) => {
|
||||||
onDelta: (text) => {
|
streamState.outputText += text;
|
||||||
streamState.outputText += text;
|
broadcast({ type: "DELTA", text });
|
||||||
broadcast({ type: "DELTA", text });
|
}
|
||||||
}
|
});
|
||||||
});
|
} else {
|
||||||
|
await streamChatCompletion({
|
||||||
|
apiKey,
|
||||||
|
apiBaseUrl,
|
||||||
|
apiKeyHeader,
|
||||||
|
apiKeyPrefix,
|
||||||
|
model,
|
||||||
|
systemPrompt: systemPrompt || "",
|
||||||
|
userMessage,
|
||||||
|
signal,
|
||||||
|
onDelta: (text) => {
|
||||||
|
streamState.outputText += text;
|
||||||
|
broadcast({ type: "DELTA", text });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
broadcast({ type: "DONE" });
|
broadcast({ type: "DONE" });
|
||||||
} finally {
|
} finally {
|
||||||
@@ -268,6 +393,73 @@ function buildAuthHeader(apiKeyHeader, apiKeyPrefix, apiKey) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function replaceQuotedToken(template, token, value) {
|
||||||
|
const quoted = `"${token}"`;
|
||||||
|
const jsonValue = JSON.stringify(value ?? "");
|
||||||
|
return template.split(quoted).join(jsonValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceTemplateTokens(template, replacements) {
|
||||||
|
let output = template || "";
|
||||||
|
for (const [token, value] of Object.entries(replacements)) {
|
||||||
|
output = replaceQuotedToken(output, token, value ?? "");
|
||||||
|
output = output.split(token).join(value ?? "");
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceUrlTokens(url, replacements) {
|
||||||
|
let output = url || "";
|
||||||
|
for (const [token, value] of Object.entries(replacements)) {
|
||||||
|
output = output.split(token).join(encodeURIComponent(value ?? ""));
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTemplateBody(template, replacements) {
|
||||||
|
const filled = replaceTemplateTokens(template, replacements);
|
||||||
|
try {
|
||||||
|
return JSON.parse(filled);
|
||||||
|
} catch {
|
||||||
|
throw new Error("Invalid request template JSON.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readSseStream(response, onDelta) {
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = "";
|
||||||
|
|
||||||
|
// OpenAI-compatible SSE stream; parse incremental deltas from data lines.
|
||||||
|
while (true) {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
const lines = buffer.split("\n");
|
||||||
|
buffer = lines.pop() || "";
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed.startsWith("data:")) continue;
|
||||||
|
|
||||||
|
const data = trimmed.slice(5).trim();
|
||||||
|
if (!data) continue;
|
||||||
|
if (data === "[DONE]") return;
|
||||||
|
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(data);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delta = parsed?.choices?.[0]?.delta?.content;
|
||||||
|
if (delta) onDelta(delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function streamChatCompletion({
|
async function streamChatCompletion({
|
||||||
apiKey,
|
apiKey,
|
||||||
apiBaseUrl,
|
apiBaseUrl,
|
||||||
@@ -309,39 +501,42 @@ async function streamChatCompletion({
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
throw new Error(`OpenAI API error ${response.status}: ${errorText}`);
|
throw new Error(`API error ${response.status}: ${errorText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
await readSseStream(response, onDelta);
|
||||||
const decoder = new TextDecoder();
|
}
|
||||||
let buffer = "";
|
|
||||||
|
async function streamCustomCompletion({
|
||||||
// OpenAI streams Server-Sent Events; parse incremental deltas from data lines.
|
apiKey,
|
||||||
while (true) {
|
apiUrl,
|
||||||
const { value, done } = await reader.read();
|
requestTemplate,
|
||||||
if (done) break;
|
systemPrompt,
|
||||||
|
userMessage,
|
||||||
buffer += decoder.decode(value, { stream: true });
|
signal,
|
||||||
const lines = buffer.split("\n");
|
onDelta
|
||||||
buffer = lines.pop() || "";
|
}) {
|
||||||
|
const replacements = {
|
||||||
for (const line of lines) {
|
PROMPT_GOES_HERE: userMessage,
|
||||||
const trimmed = line.trim();
|
SYSTEM_PROMPT_GOES_HERE: systemPrompt,
|
||||||
if (!trimmed.startsWith("data:")) continue;
|
API_KEY_GOES_HERE: apiKey
|
||||||
|
};
|
||||||
const data = trimmed.slice(5).trim();
|
const resolvedUrl = replaceUrlTokens(apiUrl, replacements);
|
||||||
if (!data) continue;
|
const body = buildTemplateBody(requestTemplate, replacements);
|
||||||
if (data === "[DONE]") return;
|
|
||||||
|
const response = await fetch(resolvedUrl, {
|
||||||
let parsed;
|
method: "POST",
|
||||||
try {
|
headers: {
|
||||||
parsed = JSON.parse(data);
|
"Content-Type": "application/json"
|
||||||
} catch {
|
},
|
||||||
continue;
|
body: JSON.stringify(body),
|
||||||
}
|
signal
|
||||||
|
});
|
||||||
const delta = parsed?.choices?.[0]?.delta?.content;
|
|
||||||
if (delta) onDelta(delta);
|
if (!response.ok) {
|
||||||
}
|
const errorText = await response.text();
|
||||||
}
|
throw new Error(`API error ${response.status}: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await readSseStream(response, onDelta);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -431,6 +431,8 @@ async function handleAnalyze() {
|
|||||||
const {
|
const {
|
||||||
apiKeys = [],
|
apiKeys = [],
|
||||||
activeApiKeyId = "",
|
activeApiKeyId = "",
|
||||||
|
apiConfigs = [],
|
||||||
|
activeApiConfigId = "",
|
||||||
apiBaseUrl,
|
apiBaseUrl,
|
||||||
apiKeyHeader,
|
apiKeyHeader,
|
||||||
apiKeyPrefix,
|
apiKeyPrefix,
|
||||||
@@ -440,6 +442,8 @@ async function handleAnalyze() {
|
|||||||
} = await getStorage([
|
} = await getStorage([
|
||||||
"apiKeys",
|
"apiKeys",
|
||||||
"activeApiKeyId",
|
"activeApiKeyId",
|
||||||
|
"apiConfigs",
|
||||||
|
"activeApiConfigId",
|
||||||
"apiBaseUrl",
|
"apiBaseUrl",
|
||||||
"apiKeyHeader",
|
"apiKeyHeader",
|
||||||
"apiKeyPrefix",
|
"apiKeyPrefix",
|
||||||
@@ -448,24 +452,56 @@ async function handleAnalyze() {
|
|||||||
"resume"
|
"resume"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!apiBaseUrl) {
|
const resolvedConfigs = Array.isArray(apiConfigs) ? apiConfigs : [];
|
||||||
setStatus("Set an API base URL in Settings.");
|
const activeConfig =
|
||||||
return;
|
resolvedConfigs.find((entry) => entry.id === activeApiConfigId) ||
|
||||||
}
|
resolvedConfigs[0];
|
||||||
|
const isAdvanced = Boolean(activeConfig?.advanced);
|
||||||
|
const resolvedApiUrl = activeConfig?.apiUrl || "";
|
||||||
|
const resolvedTemplate = activeConfig?.requestTemplate || "";
|
||||||
|
const resolvedApiBaseUrl = isAdvanced
|
||||||
|
? ""
|
||||||
|
: activeConfig?.apiBaseUrl || apiBaseUrl || "";
|
||||||
|
const resolvedApiKeyHeader = isAdvanced
|
||||||
|
? ""
|
||||||
|
: activeConfig?.apiKeyHeader ?? apiKeyHeader ?? "";
|
||||||
|
const resolvedApiKeyPrefix = isAdvanced
|
||||||
|
? ""
|
||||||
|
: activeConfig?.apiKeyPrefix ?? apiKeyPrefix ?? "";
|
||||||
|
const resolvedModel = isAdvanced ? "" : activeConfig?.model || model || "";
|
||||||
|
|
||||||
const resolvedKeys = Array.isArray(apiKeys) ? apiKeys : [];
|
const resolvedKeys = Array.isArray(apiKeys) ? apiKeys : [];
|
||||||
const activeKey =
|
const resolvedKeyId =
|
||||||
resolvedKeys.find((entry) => entry.id === activeApiKeyId) || resolvedKeys[0];
|
activeConfig?.apiKeyId || activeApiKeyId || resolvedKeys[0]?.id || "";
|
||||||
|
const activeKey = resolvedKeys.find((entry) => entry.id === resolvedKeyId);
|
||||||
const apiKey = activeKey?.key || "";
|
const apiKey = activeKey?.key || "";
|
||||||
|
|
||||||
if (apiKeyHeader && !apiKey) {
|
if (isAdvanced) {
|
||||||
setStatus("Add an API key in Settings.");
|
if (!resolvedApiUrl) {
|
||||||
return;
|
setStatus("Set an API URL in Settings.");
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
if (!model) {
|
if (!resolvedTemplate) {
|
||||||
setStatus("Set a model name in Settings.");
|
setStatus("Set a request template in Settings.");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
if (resolvedTemplate.includes("API_KEY_GOES_HERE") && !apiKey) {
|
||||||
|
setStatus("Add an API key in Settings.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!resolvedApiBaseUrl) {
|
||||||
|
setStatus("Set an API base URL in Settings.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (resolvedApiKeyHeader && !apiKey) {
|
||||||
|
setStatus("Add an API key in Settings.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!resolvedModel) {
|
||||||
|
setStatus("Set a model name in Settings.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const promptText = buildUserMessage(resume || "", task.text || "", state.postingText);
|
const promptText = buildUserMessage(resume || "", task.text || "", state.postingText);
|
||||||
@@ -481,10 +517,13 @@ async function handleAnalyze() {
|
|||||||
type: "START_ANALYSIS",
|
type: "START_ANALYSIS",
|
||||||
payload: {
|
payload: {
|
||||||
apiKey,
|
apiKey,
|
||||||
apiBaseUrl,
|
apiMode: isAdvanced ? "advanced" : "basic",
|
||||||
apiKeyHeader,
|
apiUrl: resolvedApiUrl,
|
||||||
apiKeyPrefix,
|
requestTemplate: resolvedTemplate,
|
||||||
model,
|
apiBaseUrl: resolvedApiBaseUrl,
|
||||||
|
apiKeyHeader: resolvedApiKeyHeader,
|
||||||
|
apiKeyPrefix: resolvedApiKeyPrefix,
|
||||||
|
model: resolvedModel,
|
||||||
systemPrompt: systemPrompt || "",
|
systemPrompt: systemPrompt || "",
|
||||||
resume: resume || "",
|
resume: resume || "",
|
||||||
taskText: task.text || "",
|
taskText: task.text || "",
|
||||||
|
|||||||
@@ -124,6 +124,11 @@ body {
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.row-title {
|
.row-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
@@ -249,8 +254,40 @@ button:active {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.api-key-actions .delete {
|
.api-key-actions .delete,
|
||||||
color: #c0392b;
|
.api-config-actions .delete,
|
||||||
|
.task-actions .delete {
|
||||||
|
background: #c0392b;
|
||||||
|
border-color: #c0392b;
|
||||||
|
color: #fff6f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-configs {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-config-card {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--card-bg);
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-config-card.is-advanced .basic-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-config-card:not(.is-advanced) .advanced-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-config-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@@ -275,7 +312,3 @@ button:active {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-actions .delete {
|
|
||||||
color: #c0392b;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,32 +27,15 @@
|
|||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div></div>
|
<div></div>
|
||||||
<button id="resetApiBtn" class="ghost" type="button">Reset to OpenAI</button>
|
<div class="row-actions">
|
||||||
|
<button id="addApiConfigBtn" class="ghost" type="button">Add Config</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="activeApiKeySelect">Active key</label>
|
<label for="activeApiConfigSelect">Active config</label>
|
||||||
<select id="activeApiKeySelect"></select>
|
<select id="activeApiConfigSelect"></select>
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="apiBaseUrl">API Base URL</label>
|
|
||||||
<input
|
|
||||||
id="apiBaseUrl"
|
|
||||||
type="text"
|
|
||||||
placeholder="https://api.openai.com/v1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="apiKeyHeader">API Key Header</label>
|
|
||||||
<input id="apiKeyHeader" type="text" placeholder="Authorization" />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="apiKeyPrefix">API Key Prefix</label>
|
|
||||||
<input id="apiKeyPrefix" type="text" placeholder="Bearer " />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="model">Model name</label>
|
|
||||||
<input id="model" type="text" placeholder="gpt-4o-mini" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<div id="apiConfigs" class="api-configs"></div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -62,7 +45,7 @@
|
|||||||
<span class="caret-closed">▸</span>
|
<span class="caret-closed">▸</span>
|
||||||
<span class="caret-open">▾</span>
|
<span class="caret-open">▾</span>
|
||||||
</span>
|
</span>
|
||||||
<h2>API Keys</h2>
|
<h2>API KEYS</h2>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
const apiBaseUrlInput = document.getElementById("apiBaseUrl");
|
|
||||||
const apiKeyHeaderInput = document.getElementById("apiKeyHeader");
|
|
||||||
const apiKeyPrefixInput = document.getElementById("apiKeyPrefix");
|
|
||||||
const modelInput = document.getElementById("model");
|
|
||||||
const systemPromptInput = document.getElementById("systemPrompt");
|
const systemPromptInput = document.getElementById("systemPrompt");
|
||||||
const resumeInput = document.getElementById("resume");
|
const resumeInput = document.getElementById("resume");
|
||||||
const saveBtn = document.getElementById("saveBtn");
|
const saveBtn = document.getElementById("saveBtn");
|
||||||
|
const addApiConfigBtn = document.getElementById("addApiConfigBtn");
|
||||||
|
const apiConfigsContainer = document.getElementById("apiConfigs");
|
||||||
|
const activeApiConfigSelect = document.getElementById("activeApiConfigSelect");
|
||||||
const addApiKeyBtn = document.getElementById("addApiKeyBtn");
|
const addApiKeyBtn = document.getElementById("addApiKeyBtn");
|
||||||
const apiKeysContainer = document.getElementById("apiKeys");
|
const apiKeysContainer = document.getElementById("apiKeys");
|
||||||
const activeApiKeySelect = document.getElementById("activeApiKeySelect");
|
|
||||||
const addTaskBtn = document.getElementById("addTaskBtn");
|
const addTaskBtn = document.getElementById("addTaskBtn");
|
||||||
const tasksContainer = document.getElementById("tasks");
|
const tasksContainer = document.getElementById("tasks");
|
||||||
const statusEl = document.getElementById("status");
|
const statusEl = document.getElementById("status");
|
||||||
const themeSelect = document.getElementById("themeSelect");
|
const themeSelect = document.getElementById("themeSelect");
|
||||||
const resetApiBtn = document.getElementById("resetApiBtn");
|
|
||||||
|
|
||||||
const OPENAI_DEFAULTS = {
|
const OPENAI_DEFAULTS = {
|
||||||
apiBaseUrl: "https://api.openai.com/v1",
|
apiBaseUrl: "https://api.openai.com/v1",
|
||||||
apiKeyHeader: "Authorization",
|
apiKeyHeader: "Authorization",
|
||||||
apiKeyPrefix: "Bearer "
|
apiKeyPrefix: "Bearer "
|
||||||
};
|
};
|
||||||
|
const DEFAULT_MODEL = "gpt-4o-mini";
|
||||||
|
|
||||||
function getStorage(keys) {
|
function getStorage(keys) {
|
||||||
return new Promise((resolve) => chrome.storage.local.get(keys, resolve));
|
return new Promise((resolve) => chrome.storage.local.get(keys, resolve));
|
||||||
@@ -47,6 +45,308 @@ function newApiKeyId() {
|
|||||||
return `key-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
return `key-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function newApiConfigId() {
|
||||||
|
if (crypto?.randomUUID) return crypto.randomUUID();
|
||||||
|
return `config-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectNames(container, selector) {
|
||||||
|
if (!container) return [];
|
||||||
|
return [...container.querySelectorAll(selector)]
|
||||||
|
.map((input) => (input.value || "").trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUniqueDefaultName(names) {
|
||||||
|
const lower = new Set(names.map((name) => name.toLowerCase()));
|
||||||
|
if (!lower.has("default")) return "Default";
|
||||||
|
let index = 2;
|
||||||
|
while (lower.has(`default-${index}`)) {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
return `Default-${index}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureUniqueName(desired, existingNames) {
|
||||||
|
const trimmed = (desired || "").trim();
|
||||||
|
const lowerNames = existingNames.map((name) => name.toLowerCase());
|
||||||
|
if (trimmed && !lowerNames.includes(trimmed.toLowerCase())) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
return buildUniqueDefaultName(existingNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setApiConfigAdvanced(card, isAdvanced) {
|
||||||
|
card.classList.toggle("is-advanced", isAdvanced);
|
||||||
|
card.dataset.mode = isAdvanced ? "advanced" : "basic";
|
||||||
|
|
||||||
|
const basicFields = card.querySelectorAll(
|
||||||
|
".basic-only input, .basic-only textarea"
|
||||||
|
);
|
||||||
|
const advancedFields = card.querySelectorAll(
|
||||||
|
".advanced-only input, .advanced-only textarea"
|
||||||
|
);
|
||||||
|
basicFields.forEach((field) => {
|
||||||
|
field.disabled = isAdvanced;
|
||||||
|
});
|
||||||
|
advancedFields.forEach((field) => {
|
||||||
|
field.disabled = !isAdvanced;
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetBtn = card.querySelector(".reset-openai");
|
||||||
|
if (resetBtn) resetBtn.disabled = isAdvanced;
|
||||||
|
|
||||||
|
const advancedBtn = card.querySelector(".advanced-toggle");
|
||||||
|
if (advancedBtn && isAdvanced) advancedBtn.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function readApiConfigFromCard(card) {
|
||||||
|
const nameInput = card.querySelector(".api-config-name");
|
||||||
|
const keySelect = card.querySelector(".api-config-key-select");
|
||||||
|
const baseInput = card.querySelector(".api-config-base");
|
||||||
|
const headerInput = card.querySelector(".api-config-header");
|
||||||
|
const prefixInput = card.querySelector(".api-config-prefix");
|
||||||
|
const modelInput = card.querySelector(".api-config-model");
|
||||||
|
const urlInput = card.querySelector(".api-config-url");
|
||||||
|
const templateInput = card.querySelector(".api-config-template");
|
||||||
|
const isAdvanced = card.classList.contains("is-advanced");
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: card.dataset.id || newApiConfigId(),
|
||||||
|
name: (nameInput?.value || "Default").trim(),
|
||||||
|
apiKeyId: keySelect?.value || "",
|
||||||
|
apiBaseUrl: (baseInput?.value || "").trim(),
|
||||||
|
apiKeyHeader: (headerInput?.value || "").trim(),
|
||||||
|
apiKeyPrefix: prefixInput?.value || "",
|
||||||
|
model: (modelInput?.value || "").trim(),
|
||||||
|
apiUrl: (urlInput?.value || "").trim(),
|
||||||
|
requestTemplate: (templateInput?.value || "").trim(),
|
||||||
|
advanced: isAdvanced
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildApiConfigCard(config) {
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "api-config-card";
|
||||||
|
card.dataset.id = config.id || newApiConfigId();
|
||||||
|
const isAdvanced = Boolean(config.advanced);
|
||||||
|
|
||||||
|
const nameField = document.createElement("div");
|
||||||
|
nameField.className = "field";
|
||||||
|
const nameLabel = document.createElement("label");
|
||||||
|
nameLabel.textContent = "Name";
|
||||||
|
const nameInput = document.createElement("input");
|
||||||
|
nameInput.type = "text";
|
||||||
|
nameInput.value = config.name || "";
|
||||||
|
nameInput.className = "api-config-name";
|
||||||
|
nameField.appendChild(nameLabel);
|
||||||
|
nameField.appendChild(nameInput);
|
||||||
|
|
||||||
|
const keyField = document.createElement("div");
|
||||||
|
keyField.className = "field";
|
||||||
|
const keyLabel = document.createElement("label");
|
||||||
|
keyLabel.textContent = "API Key";
|
||||||
|
const keySelect = document.createElement("select");
|
||||||
|
keySelect.className = "api-config-key-select";
|
||||||
|
keySelect.dataset.preferred = config.apiKeyId || "";
|
||||||
|
keyField.appendChild(keyLabel);
|
||||||
|
keyField.appendChild(keySelect);
|
||||||
|
|
||||||
|
const baseField = document.createElement("div");
|
||||||
|
baseField.className = "field basic-only";
|
||||||
|
const baseLabel = document.createElement("label");
|
||||||
|
baseLabel.textContent = "API Base URL";
|
||||||
|
const baseInput = document.createElement("input");
|
||||||
|
baseInput.type = "text";
|
||||||
|
baseInput.placeholder = OPENAI_DEFAULTS.apiBaseUrl;
|
||||||
|
baseInput.value = config.apiBaseUrl || "";
|
||||||
|
baseInput.className = "api-config-base";
|
||||||
|
baseField.appendChild(baseLabel);
|
||||||
|
baseField.appendChild(baseInput);
|
||||||
|
|
||||||
|
const headerField = document.createElement("div");
|
||||||
|
headerField.className = "field basic-only";
|
||||||
|
const headerLabel = document.createElement("label");
|
||||||
|
headerLabel.textContent = "API Key Header";
|
||||||
|
const headerInput = document.createElement("input");
|
||||||
|
headerInput.type = "text";
|
||||||
|
headerInput.placeholder = OPENAI_DEFAULTS.apiKeyHeader;
|
||||||
|
headerInput.value = config.apiKeyHeader || "";
|
||||||
|
headerInput.className = "api-config-header";
|
||||||
|
headerField.appendChild(headerLabel);
|
||||||
|
headerField.appendChild(headerInput);
|
||||||
|
|
||||||
|
const prefixField = document.createElement("div");
|
||||||
|
prefixField.className = "field basic-only";
|
||||||
|
const prefixLabel = document.createElement("label");
|
||||||
|
prefixLabel.textContent = "API Key Prefix";
|
||||||
|
const prefixInput = document.createElement("input");
|
||||||
|
prefixInput.type = "text";
|
||||||
|
prefixInput.placeholder = OPENAI_DEFAULTS.apiKeyPrefix;
|
||||||
|
prefixInput.value = config.apiKeyPrefix || "";
|
||||||
|
prefixInput.className = "api-config-prefix";
|
||||||
|
prefixField.appendChild(prefixLabel);
|
||||||
|
prefixField.appendChild(prefixInput);
|
||||||
|
|
||||||
|
const modelField = document.createElement("div");
|
||||||
|
modelField.className = "field basic-only";
|
||||||
|
const modelLabel = document.createElement("label");
|
||||||
|
modelLabel.textContent = "Model name";
|
||||||
|
const modelInput = document.createElement("input");
|
||||||
|
modelInput.type = "text";
|
||||||
|
modelInput.placeholder = DEFAULT_MODEL;
|
||||||
|
modelInput.value = config.model || "";
|
||||||
|
modelInput.className = "api-config-model";
|
||||||
|
modelField.appendChild(modelLabel);
|
||||||
|
modelField.appendChild(modelInput);
|
||||||
|
|
||||||
|
const urlField = document.createElement("div");
|
||||||
|
urlField.className = "field advanced-only";
|
||||||
|
const urlLabel = document.createElement("label");
|
||||||
|
urlLabel.textContent = "API URL";
|
||||||
|
const urlInput = document.createElement("input");
|
||||||
|
urlInput.type = "text";
|
||||||
|
urlInput.placeholder = "https://api.example.com/v1/chat/completions";
|
||||||
|
urlInput.value = config.apiUrl || "";
|
||||||
|
urlInput.className = "api-config-url";
|
||||||
|
urlField.appendChild(urlLabel);
|
||||||
|
urlField.appendChild(urlInput);
|
||||||
|
|
||||||
|
const templateField = document.createElement("div");
|
||||||
|
templateField.className = "field advanced-only";
|
||||||
|
const templateLabel = document.createElement("label");
|
||||||
|
templateLabel.textContent = "Request JSON template";
|
||||||
|
const templateInput = document.createElement("textarea");
|
||||||
|
templateInput.rows = 8;
|
||||||
|
templateInput.placeholder = [
|
||||||
|
"{",
|
||||||
|
" \"stream\": true,",
|
||||||
|
" \"messages\": [",
|
||||||
|
" { \"role\": \"system\", \"content\": \"SYSTEM_PROMPT_GOES_HERE\" },",
|
||||||
|
" { \"role\": \"user\", \"content\": \"PROMPT_GOES_HERE\" }",
|
||||||
|
" ],",
|
||||||
|
" \"api_key\": \"API_KEY_GOES_HERE\"",
|
||||||
|
"}"
|
||||||
|
].join("\n");
|
||||||
|
templateInput.value = config.requestTemplate || "";
|
||||||
|
templateInput.className = "api-config-template";
|
||||||
|
templateField.appendChild(templateLabel);
|
||||||
|
templateField.appendChild(templateInput);
|
||||||
|
|
||||||
|
const actions = document.createElement("div");
|
||||||
|
actions.className = "api-config-actions";
|
||||||
|
if (!isAdvanced) {
|
||||||
|
const advancedBtn = document.createElement("button");
|
||||||
|
advancedBtn.type = "button";
|
||||||
|
advancedBtn.className = "ghost advanced-toggle";
|
||||||
|
advancedBtn.textContent = "Advanced Mode";
|
||||||
|
advancedBtn.addEventListener("click", () => {
|
||||||
|
setApiConfigAdvanced(card, true);
|
||||||
|
updateApiConfigSelect(activeApiConfigSelect.value);
|
||||||
|
});
|
||||||
|
actions.appendChild(advancedBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicateBtn = document.createElement("button");
|
||||||
|
duplicateBtn.type = "button";
|
||||||
|
duplicateBtn.className = "ghost duplicate";
|
||||||
|
duplicateBtn.textContent = "Duplicate";
|
||||||
|
duplicateBtn.addEventListener("click", () => {
|
||||||
|
const names = collectNames(apiConfigsContainer, ".api-config-name");
|
||||||
|
const copy = readApiConfigFromCard(card);
|
||||||
|
copy.id = newApiConfigId();
|
||||||
|
copy.name = ensureUniqueName(`${copy.name || "Default"} Copy`, names);
|
||||||
|
const newCard = buildApiConfigCard(copy);
|
||||||
|
card.insertAdjacentElement("afterend", newCard);
|
||||||
|
updateApiConfigKeyOptions();
|
||||||
|
updateApiConfigSelect(newCard.dataset.id);
|
||||||
|
});
|
||||||
|
actions.appendChild(duplicateBtn);
|
||||||
|
|
||||||
|
const resetBtn = document.createElement("button");
|
||||||
|
resetBtn.type = "button";
|
||||||
|
resetBtn.className = "ghost reset-openai";
|
||||||
|
resetBtn.textContent = "Reset to OpenAI";
|
||||||
|
resetBtn.addEventListener("click", () => {
|
||||||
|
if (card.classList.contains("is-advanced")) {
|
||||||
|
setStatus("Advanced mode cannot be reset to OpenAI.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
baseInput.value = OPENAI_DEFAULTS.apiBaseUrl;
|
||||||
|
headerInput.value = OPENAI_DEFAULTS.apiKeyHeader;
|
||||||
|
prefixInput.value = OPENAI_DEFAULTS.apiKeyPrefix;
|
||||||
|
updateApiConfigSelect(activeApiConfigSelect.value);
|
||||||
|
});
|
||||||
|
actions.appendChild(resetBtn);
|
||||||
|
|
||||||
|
const deleteBtn = document.createElement("button");
|
||||||
|
deleteBtn.type = "button";
|
||||||
|
deleteBtn.className = "ghost delete";
|
||||||
|
deleteBtn.textContent = "Delete";
|
||||||
|
deleteBtn.addEventListener("click", () => {
|
||||||
|
card.remove();
|
||||||
|
updateApiConfigSelect(activeApiConfigSelect.value);
|
||||||
|
});
|
||||||
|
actions.appendChild(deleteBtn);
|
||||||
|
|
||||||
|
const updateSelect = () => updateApiConfigSelect(activeApiConfigSelect.value);
|
||||||
|
nameInput.addEventListener("input", updateSelect);
|
||||||
|
baseInput.addEventListener("input", updateSelect);
|
||||||
|
headerInput.addEventListener("input", updateSelect);
|
||||||
|
prefixInput.addEventListener("input", updateSelect);
|
||||||
|
modelInput.addEventListener("input", updateSelect);
|
||||||
|
urlInput.addEventListener("input", updateSelect);
|
||||||
|
templateInput.addEventListener("input", updateSelect);
|
||||||
|
|
||||||
|
card.appendChild(nameField);
|
||||||
|
card.appendChild(keyField);
|
||||||
|
card.appendChild(baseField);
|
||||||
|
card.appendChild(headerField);
|
||||||
|
card.appendChild(prefixField);
|
||||||
|
card.appendChild(modelField);
|
||||||
|
card.appendChild(urlField);
|
||||||
|
card.appendChild(templateField);
|
||||||
|
card.appendChild(actions);
|
||||||
|
|
||||||
|
setApiConfigAdvanced(card, isAdvanced);
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectApiConfigs() {
|
||||||
|
const cards = [...apiConfigsContainer.querySelectorAll(".api-config-card")];
|
||||||
|
return cards.map((card) => readApiConfigFromCard(card));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateApiConfigSelect(preferredId) {
|
||||||
|
const configs = collectApiConfigs();
|
||||||
|
activeApiConfigSelect.innerHTML = "";
|
||||||
|
|
||||||
|
if (!configs.length) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = "";
|
||||||
|
option.textContent = "No configs configured";
|
||||||
|
activeApiConfigSelect.appendChild(option);
|
||||||
|
activeApiConfigSelect.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeApiConfigSelect.disabled = false;
|
||||||
|
const selectedId =
|
||||||
|
preferredId && configs.some((config) => config.id === preferredId)
|
||||||
|
? preferredId
|
||||||
|
: configs[0].id;
|
||||||
|
|
||||||
|
for (const config of configs) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = config.id;
|
||||||
|
option.textContent = config.name || "Default";
|
||||||
|
activeApiConfigSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
activeApiConfigSelect.value = selectedId;
|
||||||
|
}
|
||||||
|
|
||||||
function buildApiKeyCard(entry) {
|
function buildApiKeyCard(entry) {
|
||||||
const card = document.createElement("div");
|
const card = document.createElement("div");
|
||||||
card.className = "api-key-card";
|
card.className = "api-key-card";
|
||||||
@@ -97,11 +397,11 @@ function buildApiKeyCard(entry) {
|
|||||||
deleteBtn.textContent = "Delete";
|
deleteBtn.textContent = "Delete";
|
||||||
deleteBtn.addEventListener("click", () => {
|
deleteBtn.addEventListener("click", () => {
|
||||||
card.remove();
|
card.remove();
|
||||||
updateApiKeySelect();
|
updateApiConfigKeyOptions();
|
||||||
});
|
});
|
||||||
actions.appendChild(deleteBtn);
|
actions.appendChild(deleteBtn);
|
||||||
|
|
||||||
const updateSelect = () => updateApiKeySelect(activeApiKeySelect.value);
|
const updateSelect = () => updateApiConfigKeyOptions();
|
||||||
nameInput.addEventListener("input", updateSelect);
|
nameInput.addEventListener("input", updateSelect);
|
||||||
keyInput.addEventListener("input", updateSelect);
|
keyInput.addEventListener("input", updateSelect);
|
||||||
|
|
||||||
@@ -125,33 +425,37 @@ function collectApiKeys() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateApiKeySelect(preferredId) {
|
function updateApiConfigKeyOptions() {
|
||||||
const keys = collectApiKeys();
|
const keys = collectApiKeys();
|
||||||
activeApiKeySelect.innerHTML = "";
|
const selects = apiConfigsContainer.querySelectorAll(".api-config-key-select");
|
||||||
|
selects.forEach((select) => {
|
||||||
|
const preferred = select.dataset.preferred || select.value;
|
||||||
|
select.innerHTML = "";
|
||||||
|
if (!keys.length) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = "";
|
||||||
|
option.textContent = "No keys configured";
|
||||||
|
select.appendChild(option);
|
||||||
|
select.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!keys.length) {
|
select.disabled = false;
|
||||||
const option = document.createElement("option");
|
for (const key of keys) {
|
||||||
option.value = "";
|
const option = document.createElement("option");
|
||||||
option.textContent = "No keys configured";
|
option.value = key.id;
|
||||||
activeApiKeySelect.appendChild(option);
|
option.textContent = key.name || "Default";
|
||||||
activeApiKeySelect.disabled = true;
|
select.appendChild(option);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
activeApiKeySelect.disabled = false;
|
if (preferred && keys.some((key) => key.id === preferred)) {
|
||||||
const selectedId =
|
select.value = preferred;
|
||||||
preferredId && keys.some((key) => key.id === preferredId)
|
} else {
|
||||||
? preferredId
|
select.value = keys[0].id;
|
||||||
: keys[0].id;
|
}
|
||||||
|
|
||||||
for (const key of keys) {
|
select.dataset.preferred = select.value;
|
||||||
const option = document.createElement("option");
|
});
|
||||||
option.value = key.id;
|
|
||||||
option.textContent = key.name || "Default";
|
|
||||||
activeApiKeySelect.appendChild(option);
|
|
||||||
}
|
|
||||||
|
|
||||||
activeApiKeySelect.value = selectedId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTaskCard(task) {
|
function buildTaskCard(task) {
|
||||||
@@ -242,7 +546,10 @@ function buildTaskCard(task) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
addBelowBtn.addEventListener("click", () => {
|
addBelowBtn.addEventListener("click", () => {
|
||||||
const newCard = buildTaskCard({ id: newTaskId(), name: "", text: "" });
|
const name = buildUniqueDefaultName(
|
||||||
|
collectNames(tasksContainer, ".task-name")
|
||||||
|
);
|
||||||
|
const newCard = buildTaskCard({ id: newTaskId(), name, text: "" });
|
||||||
card.insertAdjacentElement("afterend", newCard);
|
card.insertAdjacentElement("afterend", newCard);
|
||||||
updateTaskControls();
|
updateTaskControls();
|
||||||
});
|
});
|
||||||
@@ -250,7 +557,10 @@ function buildTaskCard(task) {
|
|||||||
duplicateBtn.addEventListener("click", () => {
|
duplicateBtn.addEventListener("click", () => {
|
||||||
const copy = {
|
const copy = {
|
||||||
id: newTaskId(),
|
id: newTaskId(),
|
||||||
name: `${nameInput.value || "Untitled"} Copy`,
|
name: ensureUniqueName(
|
||||||
|
`${nameInput.value || "Untitled"} Copy`,
|
||||||
|
collectNames(tasksContainer, ".task-name")
|
||||||
|
),
|
||||||
text: textArea.value
|
text: textArea.value
|
||||||
};
|
};
|
||||||
const newCard = buildTaskCard(copy);
|
const newCard = buildTaskCard(copy);
|
||||||
@@ -307,6 +617,8 @@ async function loadSettings() {
|
|||||||
apiKey = "",
|
apiKey = "",
|
||||||
apiKeys = [],
|
apiKeys = [],
|
||||||
activeApiKeyId = "",
|
activeApiKeyId = "",
|
||||||
|
apiConfigs = [],
|
||||||
|
activeApiConfigId = "",
|
||||||
apiBaseUrl = "",
|
apiBaseUrl = "",
|
||||||
apiKeyHeader = "",
|
apiKeyHeader = "",
|
||||||
apiKeyPrefix = "",
|
apiKeyPrefix = "",
|
||||||
@@ -319,6 +631,8 @@ async function loadSettings() {
|
|||||||
"apiKey",
|
"apiKey",
|
||||||
"apiKeys",
|
"apiKeys",
|
||||||
"activeApiKeyId",
|
"activeApiKeyId",
|
||||||
|
"apiConfigs",
|
||||||
|
"activeApiConfigId",
|
||||||
"apiBaseUrl",
|
"apiBaseUrl",
|
||||||
"apiKeyHeader",
|
"apiKeyHeader",
|
||||||
"apiKeyPrefix",
|
"apiKeyPrefix",
|
||||||
@@ -329,10 +643,6 @@ async function loadSettings() {
|
|||||||
"theme"
|
"theme"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
apiBaseUrlInput.value = apiBaseUrl;
|
|
||||||
apiKeyHeaderInput.value = apiKeyHeader;
|
|
||||||
apiKeyPrefixInput.value = apiKeyPrefix;
|
|
||||||
modelInput.value = model;
|
|
||||||
systemPromptInput.value = systemPrompt;
|
systemPromptInput.value = systemPrompt;
|
||||||
resumeInput.value = resume;
|
resumeInput.value = resume;
|
||||||
themeSelect.value = theme;
|
themeSelect.value = theme;
|
||||||
@@ -349,17 +659,75 @@ async function loadSettings() {
|
|||||||
apiKeys: resolvedKeys,
|
apiKeys: resolvedKeys,
|
||||||
activeApiKeyId: resolvedActiveId
|
activeApiKeyId: resolvedActiveId
|
||||||
});
|
});
|
||||||
|
} else if (resolvedKeys.length) {
|
||||||
|
const hasActive = resolvedKeys.some((entry) => entry.id === resolvedActiveId);
|
||||||
|
if (!hasActive) {
|
||||||
|
resolvedActiveId = resolvedKeys[0].id;
|
||||||
|
await chrome.storage.local.set({ activeApiKeyId: resolvedActiveId });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apiKeysContainer.innerHTML = "";
|
apiKeysContainer.innerHTML = "";
|
||||||
if (!resolvedKeys.length) {
|
if (!resolvedKeys.length) {
|
||||||
apiKeysContainer.appendChild(buildApiKeyCard({ id: newApiKeyId(), name: "", key: "" }));
|
apiKeysContainer.appendChild(
|
||||||
|
buildApiKeyCard({ id: newApiKeyId(), name: "", key: "" })
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
for (const entry of resolvedKeys) {
|
for (const entry of resolvedKeys) {
|
||||||
apiKeysContainer.appendChild(buildApiKeyCard(entry));
|
apiKeysContainer.appendChild(buildApiKeyCard(entry));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateApiKeySelect(resolvedActiveId);
|
|
||||||
|
let resolvedConfigs = Array.isArray(apiConfigs) ? apiConfigs : [];
|
||||||
|
let resolvedActiveConfigId = activeApiConfigId;
|
||||||
|
|
||||||
|
if (!resolvedConfigs.length) {
|
||||||
|
const migrated = {
|
||||||
|
id: newApiConfigId(),
|
||||||
|
name: "Default",
|
||||||
|
apiBaseUrl: apiBaseUrl || OPENAI_DEFAULTS.apiBaseUrl,
|
||||||
|
apiKeyHeader: apiKeyHeader || OPENAI_DEFAULTS.apiKeyHeader,
|
||||||
|
apiKeyPrefix: apiKeyPrefix || OPENAI_DEFAULTS.apiKeyPrefix,
|
||||||
|
model: model || DEFAULT_MODEL,
|
||||||
|
apiKeyId: resolvedActiveId || resolvedKeys[0]?.id || "",
|
||||||
|
apiUrl: "",
|
||||||
|
requestTemplate: "",
|
||||||
|
advanced: false
|
||||||
|
};
|
||||||
|
resolvedConfigs = [migrated];
|
||||||
|
resolvedActiveConfigId = migrated.id;
|
||||||
|
await chrome.storage.local.set({
|
||||||
|
apiConfigs: resolvedConfigs,
|
||||||
|
activeApiConfigId: resolvedActiveConfigId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const fallbackKeyId = resolvedActiveId || resolvedKeys[0]?.id || "";
|
||||||
|
const withKeys = resolvedConfigs.map((config) => ({
|
||||||
|
...config,
|
||||||
|
apiKeyId: config.apiKeyId || fallbackKeyId,
|
||||||
|
apiUrl: config.apiUrl || "",
|
||||||
|
requestTemplate: config.requestTemplate || "",
|
||||||
|
advanced: Boolean(config.advanced)
|
||||||
|
}));
|
||||||
|
if (withKeys.some((config, index) => config.apiKeyId !== resolvedConfigs[index].apiKeyId)) {
|
||||||
|
resolvedConfigs = withKeys;
|
||||||
|
await chrome.storage.local.set({ apiConfigs: resolvedConfigs });
|
||||||
|
}
|
||||||
|
const hasActive = resolvedConfigs.some(
|
||||||
|
(config) => config.id === resolvedActiveConfigId
|
||||||
|
);
|
||||||
|
if (!hasActive) {
|
||||||
|
resolvedActiveConfigId = resolvedConfigs[0].id;
|
||||||
|
await chrome.storage.local.set({ activeApiConfigId: resolvedActiveConfigId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apiConfigsContainer.innerHTML = "";
|
||||||
|
for (const config of resolvedConfigs) {
|
||||||
|
apiConfigsContainer.appendChild(buildApiConfigCard(config));
|
||||||
|
}
|
||||||
|
updateApiConfigKeyOptions();
|
||||||
|
updateApiConfigSelect(resolvedActiveConfigId);
|
||||||
|
|
||||||
tasksContainer.innerHTML = "";
|
tasksContainer.innerHTML = "";
|
||||||
if (!tasks.length) {
|
if (!tasks.length) {
|
||||||
@@ -379,17 +747,21 @@ async function loadSettings() {
|
|||||||
async function saveSettings() {
|
async function saveSettings() {
|
||||||
const tasks = collectTasks();
|
const tasks = collectTasks();
|
||||||
const apiKeys = collectApiKeys();
|
const apiKeys = collectApiKeys();
|
||||||
|
const apiConfigs = collectApiConfigs();
|
||||||
|
const activeApiConfigId =
|
||||||
|
apiConfigs.find((entry) => entry.id === activeApiConfigSelect.value)?.id ||
|
||||||
|
apiConfigs[0]?.id ||
|
||||||
|
"";
|
||||||
|
const activeConfig = apiConfigs.find((entry) => entry.id === activeApiConfigId);
|
||||||
const activeApiKeyId =
|
const activeApiKeyId =
|
||||||
apiKeys.find((entry) => entry.id === activeApiKeySelect.value)?.id ||
|
activeConfig?.apiKeyId ||
|
||||||
apiKeys[0]?.id ||
|
apiKeys[0]?.id ||
|
||||||
"";
|
"";
|
||||||
await chrome.storage.local.set({
|
await chrome.storage.local.set({
|
||||||
apiBaseUrl: apiBaseUrlInput.value.trim(),
|
|
||||||
apiKeyHeader: apiKeyHeaderInput.value.trim(),
|
|
||||||
apiKeyPrefix: apiKeyPrefixInput.value,
|
|
||||||
apiKeys,
|
apiKeys,
|
||||||
activeApiKeyId,
|
activeApiKeyId,
|
||||||
model: modelInput.value.trim(),
|
apiConfigs,
|
||||||
|
activeApiConfigId,
|
||||||
systemPrompt: systemPromptInput.value,
|
systemPrompt: systemPromptInput.value,
|
||||||
resume: resumeInput.value,
|
resume: resumeInput.value,
|
||||||
tasks,
|
tasks,
|
||||||
@@ -400,7 +772,10 @@ async function saveSettings() {
|
|||||||
|
|
||||||
saveBtn.addEventListener("click", () => void saveSettings());
|
saveBtn.addEventListener("click", () => void saveSettings());
|
||||||
addTaskBtn.addEventListener("click", () => {
|
addTaskBtn.addEventListener("click", () => {
|
||||||
const newCard = buildTaskCard({ id: newTaskId(), name: "", text: "" });
|
const name = buildUniqueDefaultName(
|
||||||
|
collectNames(tasksContainer, ".task-name")
|
||||||
|
);
|
||||||
|
const newCard = buildTaskCard({ id: newTaskId(), name, text: "" });
|
||||||
const first = tasksContainer.firstElementChild;
|
const first = tasksContainer.firstElementChild;
|
||||||
if (first) {
|
if (first) {
|
||||||
tasksContainer.insertBefore(newCard, first);
|
tasksContainer.insertBefore(newCard, first);
|
||||||
@@ -411,31 +786,49 @@ addTaskBtn.addEventListener("click", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
addApiKeyBtn.addEventListener("click", () => {
|
addApiKeyBtn.addEventListener("click", () => {
|
||||||
const newCard = buildApiKeyCard({ id: newApiKeyId(), name: "", key: "" });
|
const name = buildUniqueDefaultName(
|
||||||
|
collectNames(apiKeysContainer, ".api-key-name")
|
||||||
|
);
|
||||||
|
const newCard = buildApiKeyCard({ id: newApiKeyId(), name, key: "" });
|
||||||
const first = apiKeysContainer.firstElementChild;
|
const first = apiKeysContainer.firstElementChild;
|
||||||
if (first) {
|
if (first) {
|
||||||
apiKeysContainer.insertBefore(newCard, first);
|
apiKeysContainer.insertBefore(newCard, first);
|
||||||
} else {
|
} else {
|
||||||
apiKeysContainer.appendChild(newCard);
|
apiKeysContainer.appendChild(newCard);
|
||||||
}
|
}
|
||||||
updateApiKeySelect(activeApiKeySelect.value);
|
updateApiConfigKeyOptions();
|
||||||
});
|
});
|
||||||
|
|
||||||
activeApiKeySelect.addEventListener("change", () => {
|
addApiConfigBtn.addEventListener("click", () => {
|
||||||
updateApiKeySelect(activeApiKeySelect.value);
|
const name = buildUniqueDefaultName(
|
||||||
|
collectNames(apiConfigsContainer, ".api-config-name")
|
||||||
|
);
|
||||||
|
const newCard = buildApiConfigCard({
|
||||||
|
id: newApiConfigId(),
|
||||||
|
name,
|
||||||
|
apiBaseUrl: OPENAI_DEFAULTS.apiBaseUrl,
|
||||||
|
apiKeyHeader: OPENAI_DEFAULTS.apiKeyHeader,
|
||||||
|
apiKeyPrefix: OPENAI_DEFAULTS.apiKeyPrefix,
|
||||||
|
model: DEFAULT_MODEL,
|
||||||
|
apiUrl: "",
|
||||||
|
requestTemplate: "",
|
||||||
|
advanced: false
|
||||||
|
});
|
||||||
|
const first = apiConfigsContainer.firstElementChild;
|
||||||
|
if (first) {
|
||||||
|
apiConfigsContainer.insertBefore(newCard, first);
|
||||||
|
} else {
|
||||||
|
apiConfigsContainer.appendChild(newCard);
|
||||||
|
}
|
||||||
|
updateApiConfigKeyOptions();
|
||||||
|
updateApiConfigSelect(activeApiConfigSelect.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
activeApiConfigSelect.addEventListener("change", () => {
|
||||||
|
updateApiConfigSelect(activeApiConfigSelect.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
themeSelect.addEventListener("change", () => applyTheme(themeSelect.value));
|
themeSelect.addEventListener("change", () => applyTheme(themeSelect.value));
|
||||||
resetApiBtn.addEventListener("click", async () => {
|
themeSelect.addEventListener("change", () => applyTheme(themeSelect.value));
|
||||||
apiBaseUrlInput.value = OPENAI_DEFAULTS.apiBaseUrl;
|
|
||||||
apiKeyHeaderInput.value = OPENAI_DEFAULTS.apiKeyHeader;
|
|
||||||
apiKeyPrefixInput.value = OPENAI_DEFAULTS.apiKeyPrefix;
|
|
||||||
await chrome.storage.local.set({
|
|
||||||
apiBaseUrl: OPENAI_DEFAULTS.apiBaseUrl,
|
|
||||||
apiKeyHeader: OPENAI_DEFAULTS.apiKeyHeader,
|
|
||||||
apiKeyPrefix: OPENAI_DEFAULTS.apiKeyPrefix
|
|
||||||
});
|
|
||||||
setStatus("OpenAI defaults restored.");
|
|
||||||
});
|
|
||||||
|
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
|||||||
Reference in New Issue
Block a user