aarnal80 commited on
Commit
b565510
verified
1 Parent(s): 4c9a302

Update js/iaConfigModule.js

Browse files
Files changed (1) hide show
  1. js/iaConfigModule.js +434 -152
js/iaConfigModule.js CHANGED
@@ -1,4 +1,5 @@
1
  // js/iaConfigModule.js
 
2
  const defaultConfig = {
3
  llm: {
4
  provider: "deepseek",
@@ -12,242 +13,523 @@ const defaultConfig = {
12
  }
13
  };
14
 
15
- // CORRECCI脫N: Lista de modelos de IA actualizada a nombres de API realistas para 2025.
16
  export const llmProviders = [
17
- {
18
- name: "OpenAI",
19
- value: "openai",
20
  models: [
21
- "gpt-5-pro",
22
- "gpt-5",
23
- "gpt-5-mini", // Nombres plausibles para la serie GPT-5
24
  "gpt-4o",
25
- "gpt-4o-mini"
 
 
 
 
 
26
  ],
27
- url: "https://api.openai.com"
28
  },
29
- {
30
- name: "DeepSeek",
31
- value: "deepseek",
32
- models: ["deepseek-chat", "deepseek-reasoner"],
33
- url: "https://api.deepseek.com"
34
  }
35
  ];
36
 
37
  export const transcriptionProviders = [
38
- { name: "OpenAI Whisper", value: "openai", models: ["whisper-1"], url: "https://api.openai.com" },
39
- { name: "Deepgram", value: "deepgram", models: ["nova-2", "whisper-large"], url: "https://api.deepgram.com" }
 
 
 
 
 
 
 
 
 
 
40
  ];
41
 
42
  function saveConfig(config) {
43
  try {
 
 
 
 
 
 
 
 
 
 
 
 
44
  localStorage.setItem("iaConfig", JSON.stringify(config));
 
 
45
  } catch (e) {
46
  console.error("[iaConfigModule] Error guardando config:", e);
 
47
  }
48
  }
49
 
50
  function clone(obj) {
51
- try { return structuredClone(obj); } catch { return JSON.parse(JSON.stringify(obj)); }
 
 
 
 
 
52
  }
53
 
54
  function loadConfig() {
55
  let config;
56
  try {
57
- config = JSON.parse(localStorage.getItem("iaConfig")) || clone(defaultConfig);
58
- } catch {
 
 
 
 
 
 
 
 
 
 
59
  config = clone(defaultConfig);
60
  }
61
 
62
- // CORRECCI脫N: La l贸gica de migraci贸n para la transcripci贸n era defectuosa.
63
- // No preservaba la API key existente si el proveedor era Deepgram.
64
- // Ahora usa el mismo patr贸n robusto que la migraci贸n de LLM.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  if (config.transcription.apiKey !== undefined) {
66
- console.log("[iaConfigModule] Migrando configuraci贸n de transcripci贸n antigua...");
67
  const oldKey = config.transcription.apiKey;
68
- const oldModel = config.transcription.model;
69
- const oldProvider = config.transcription.provider;
70
-
71
- config.transcription.apiKeys = { ...defaultConfig.transcription.apiKeys, [oldProvider]: oldKey };
72
- config.transcription.models = { ...defaultConfig.transcription.models, [oldProvider]: oldModel };
73
-
74
  delete config.transcription.apiKey;
75
- delete config.transcription.model;
76
  saveConfig(config);
 
77
  }
78
 
79
- // Migraci贸n LLM apiKey -> apiKeys (esta parte estaba bien)
80
  if (config.llm.apiKey !== undefined) {
81
- console.log("[iaConfigModule] Migrando configuraci贸n de LLM antigua...");
82
- const old = config.llm.apiKey;
83
- config.llm.apiKeys = { ...defaultConfig.llm.apiKeys, [config.llm.provider]: old };
 
 
 
84
  delete config.llm.apiKey;
85
  saveConfig(config);
 
86
  }
87
 
88
- // Migraciones de modelos obsoletos (estas partes estaban bien)
89
- if (config.llm.provider === "deepseek" && (config.llm.model === "deepseek-v3" || config.llm.model === "deepseek-llm")) {
90
- config.llm.model = "deepseek-chat";
91
- saveConfig(config);
 
 
 
 
92
  }
93
- if (config.llm.provider === "openai" && /gpt-5.*2025/.test(config.llm.model)) {
94
- config.llm.model = config.llm.model.replace(/-2025-\d{2}-\d{2}/, "");
95
- saveConfig(config);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  }
97
 
98
  return config;
99
  }
 
100
  export function getIaConfig() {
101
  return loadConfig();
102
  }
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  export function renderIaConfigForm(containerId) {
105
  let config = loadConfig();
106
  const container = document.getElementById(containerId);
 
107
  if (!container) {
108
  console.error(`[iaConfigModule] No se encontr贸 el contenedor '${containerId}'`);
109
- document.body.insertAdjacentHTML("beforeend", `<div style='color:red'>[Error] No se encontr贸 el contenedor '${containerId}'.</div>`);
 
 
 
110
  return;
111
  }
112
 
113
  function maskApiKey(key) {
114
- if (!key) return "";
115
- if (key.length <= 8) return "*".repeat(key.length);
116
- return `${key.substring(0, 3)}-****-${key.slice(-4)}`;
 
 
 
 
 
 
 
 
 
 
 
 
117
  }
118
 
119
  container.innerHTML = `
120
  <div class="flex justify-between items-center mb-6 border-b pb-2 border-blue-100">
121
- <h2 class="text-xl font-bold text-blue-700 flex items-center"><i class='fas fa-cogs mr-2'></i>Configurar Proveedores IA</h2>
122
- <button id="btnCloseConfig" type="button" class="text-gray-500 hover:text-blue-600 text-2xl focus:outline-none" aria-label="Cerrar"><i class="fas fa-times"></i></button>
 
 
 
 
123
  </div>
 
124
  <form id="iaConfigForm" class="space-y-6">
 
125
  <div class="bg-blue-50 p-4 rounded-lg border border-blue-100">
126
- <label class="block font-semibold text-blue-800 mb-2">Proveedor LLM</label>
127
- <select id="llmProvider" class="w-full mb-3 p-2 rounded border border-gray-300 focus:ring-2 focus:ring-blue-300">${llmProviders.map(p => `<option value="${p.value}">${p.name}</option>`).join("")}</select>
 
 
 
 
128
  <div class="flex items-center mb-3">
129
- <input type="password" id="llmApiKey" class="flex-1 p-2 rounded border border-gray-300 mr-2 bg-gray-100" placeholder="API Key LLM" autocomplete="off">
130
- <button class="text-blue-700 hover:text-blue-900 px-3 py-2 rounded focus:outline-none border border-blue-200 bg-white" type="button" id="toggleLlmApiKey"><i class="fas fa-eye"></i></button>
 
 
 
 
131
  </div>
132
  <select id="llmModel" class="w-full p-2 rounded border border-gray-300 focus:ring-2 focus:ring-blue-300"></select>
 
133
  </div>
 
 
134
  <div class="bg-purple-50 p-4 rounded-lg border border-purple-100">
135
- <label class="block font-semibold text-purple-800 mb-2">Proveedor Transcripci贸n</label>
136
- <select id="transProvider" class="w-full mb-3 p-2 rounded border border-gray-300 focus:ring-2 focus:ring-purple-300">${transcriptionProviders.map(p => `<option value="${p.value}">${p.name}</option>`).join("")}</select>
 
 
 
 
137
  <div class="flex items-center mb-3">
138
- <input type="password" id="transApiKey" class="flex-1 p-2 rounded border border-gray-300 mr-2 bg-gray-100" placeholder="API Key Transcripci贸n" autocomplete="off">
139
- <button class="text-purple-700 hover:text-purple-900 px-3 py-2 rounded focus:outline-none border border-purple-200 bg-white" type="button" id="toggleTransApiKey"><i class="fas fa-eye"></i></button>
 
 
 
 
140
  </div>
141
  <select id="transModel" class="w-full p-2 rounded border border-gray-300 focus:ring-2 focus:ring-purple-300"></select>
 
142
  </div>
143
- <button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 rounded-lg shadow transition-colors flex items-center justify-center text-lg"><i class="fas fa-save mr-2"></i>Guardar configuraci贸n</button>
144
- </form>`;
145
-
146
- const closeBtn = document.getElementById("btnCloseConfig");
147
- if (closeBtn) {
148
- closeBtn.addEventListener("click", () => document.getElementById("configModal")?.classList.remove("active"));
149
- }
150
-
151
- const llmProviderSelect = document.getElementById("llmProvider");
152
- const llmModelSelect = document.getElementById("llmModel");
153
- const llmApiKeyInput = document.getElementById("llmApiKey");
154
- const transProviderSelect = document.getElementById("transProvider");
155
- const transModelSelect = document.getElementById("transModel");
156
- const transApiKeyInput = document.getElementById("transApiKey");
157
-
158
- // Toggle visibilidad de API keys
159
- document.getElementById("toggleLlmApiKey").addEventListener("click", () => { llmApiKeyInput.type = llmApiKeyInput.type === "password" ? "text" : "password"; });
160
- document.getElementById("toggleTransApiKey").addEventListener("click", () => { transApiKeyInput.type = transApiKeyInput.type === "password" ? "text" : "password"; });
161
-
162
- // MEJORA: Funciones de actualizaci贸n de UI m谩s seguras y claras.
163
- // No modifican el objeto 'config' en memoria, solo leen de 茅l.
164
- function updateLlmUI() {
165
- const selectedProvider = llmProviderSelect.value;
166
- const providerObj = llmProviders.find(p => p.value === selectedProvider);
167
- llmModelSelect.innerHTML = providerObj.models.map(m => `<option value="${m}">${m}</option>`).join("");
168
-
169
- if (selectedProvider === config.llm.provider && providerObj.models.includes(config.llm.model)) {
170
- llmModelSelect.value = config.llm.model;
171
  } else {
172
- llmModelSelect.value = providerObj.models[0];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  }
174
- llmApiKeyInput.value = maskApiKey(config.llm.apiKeys[selectedProvider] || "");
175
  }
176
 
177
- function updateTransUI() {
178
- const selectedProvider = transProviderSelect.value;
179
- const providerObj = transcriptionProviders.find(p => p.value === selectedProvider);
180
- transModelSelect.innerHTML = providerObj.models.map(m => `<option value="${m}">${m}</option>`).join("");
181
- transModelSelect.value = config.transcription.models[selectedProvider] || providerObj.models[0];
182
- transApiKeyInput.value = maskApiKey(config.transcription.apiKeys[selectedProvider] || "");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  }
184
 
185
- // Asignar listeners y estado inicial
186
- llmProviderSelect.addEventListener("change", updateLlmUI);
187
- transProviderSelect.addEventListener("change", updateTransUI);
 
 
 
 
 
 
188
 
189
- llmProviderSelect.value = config.llm.provider;
190
- transProviderSelect.value = config.transcription.provider;
191
- updateLlmUI();
192
- updateTransUI();
193
 
194
- // MEJORA: La l贸gica de guardado es m谩s limpia, construyendo nuevos objetos
195
- // de configuraci贸n con el operador 'spread' para evitar mutaciones inesperadas.
196
- document.getElementById("iaConfigForm").addEventListener("submit", e => {
197
- e.preventDefault();
198
- const prevConfig = config;
199
- const newConfig = clone(prevConfig);
200
-
201
- // Proceso para LLM
202
- const llmProv = llmProviderSelect.value;
203
- const rawLlmKey = llmApiKeyInput.value;
204
- const oldLlmKey = prevConfig.llm.apiKeys[llmProv] || "";
205
- const actualLlmKey = rawLlmKey === maskApiKey(oldLlmKey) ? oldLlmKey : rawLlmKey;
206
-
207
- newConfig.llm = {
208
- ...prevConfig.llm,
209
- provider: llmProv,
210
- model: llmModelSelect.value,
211
- apiKeys: { ...prevConfig.llm.apiKeys, [llmProv]: actualLlmKey }
212
- };
213
 
214
- // Proceso para Transcripci贸n
215
- const transProv = transProviderSelect.value;
216
- const rawTransKey = transApiKeyInput.value;
217
- const oldTransKey = prevConfig.transcription.apiKeys[transProv] || "";
218
- const actualTransKey = rawTransKey === maskApiKey(oldTransKey) ? oldTransKey : rawTransKey;
219
-
220
- newConfig.transcription = {
221
- ...prevConfig.transcription,
222
- provider: transProv,
223
- models: { ...prevConfig.transcription.models, [transProv]: transModelSelect.value },
224
- apiKeys: { ...prevConfig.transcription.apiKeys, [transProv]: actualTransKey }
225
- };
 
 
 
 
 
 
 
 
 
 
 
 
226
 
227
- // Guardar y notificar
228
- saveConfig(newConfig);
229
- config = newConfig; // Actualizar el estado en memoria
230
- document.dispatchEvent(new CustomEvent("iaConfigChanged"));
231
-
232
- // Actualizar UI post-guardado
233
- llmApiKeyInput.value = maskApiKey(newConfig.llm.apiKeys[newConfig.llm.provider] || "");
234
- transApiKeyInput.value = maskApiKey(newConfig.transcription.apiKeys[newConfig.transcription.provider] || "");
235
- llmApiKeyInput.type = "password";
236
- transApiKeyInput.type = "password";
237
-
238
- const msg = document.getElementById('iaConfigSavedMsg') || document.createElement('div');
239
- if (!msg.id) {
240
- msg.id = 'iaConfigSavedMsg';
241
- msg.className = 'fixed left-1/2 top-6 -translate-x-1/2 bg-green-500 text-white px-6 py-3 rounded shadow-lg text-lg z-50 transition-opacity duration-300';
242
- msg.innerHTML = '<i class="fas fa-check-circle mr-2"></i>隆Configuraci贸n guardada!';
243
- document.body.appendChild(msg);
244
- }
245
 
246
- msg.style.opacity = '1';
247
- msg.style.display = 'block';
248
- setTimeout(() => { msg.style.opacity = '0'; }, 1600);
249
- setTimeout(() => { if (msg.style.opacity === '0') msg.style.display = 'none'; }, 1900);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
- document.getElementById("configModal")?.classList.remove("active");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  });
253
  }
 
1
  // js/iaConfigModule.js
2
+
3
  const defaultConfig = {
4
  llm: {
5
  provider: "deepseek",
 
13
  }
14
  };
15
 
16
+ // Lista de modelos actualizada y corregida (2025)
17
  export const llmProviders = [
18
+ {
19
+ name: "OpenAI",
20
+ value: "openai",
21
  models: [
 
 
 
22
  "gpt-4o",
23
+ "gpt-4o-mini",
24
+ "gpt-4-turbo",
25
+ "gpt-4",
26
+ "gpt-3.5-turbo",
27
+ "o1-preview",
28
+ "o1-mini"
29
  ],
30
+ url: "https://api.openai.com/v1"
31
  },
32
+ {
33
+ name: "DeepSeek",
34
+ value: "deepseek",
35
+ models: ["deepseek-chat", "deepseek-reasoner"],
36
+ url: "https://api.deepseek.com/v1"
37
  }
38
  ];
39
 
40
  export const transcriptionProviders = [
41
+ {
42
+ name: "OpenAI Whisper",
43
+ value: "openai",
44
+ models: ["whisper-1"],
45
+ url: "https://api.openai.com/v1/audio/transcriptions"
46
+ },
47
+ {
48
+ name: "Deepgram",
49
+ value: "deepgram",
50
+ models: ["nova-2", "nova", "enhanced", "base"],
51
+ url: "https://api.deepgram.com/v1/listen"
52
+ }
53
  ];
54
 
55
  function saveConfig(config) {
56
  try {
57
+ // Validar que tenemos un objeto v谩lido antes de guardar
58
+ if (!config || typeof config !== 'object') {
59
+ console.error("[iaConfigModule] Configuraci贸n inv谩lida para guardar");
60
+ return false;
61
+ }
62
+
63
+ // Verificar si localStorage est谩 disponible
64
+ if (typeof Storage === "undefined") {
65
+ console.error("[iaConfigModule] LocalStorage no disponible");
66
+ return false;
67
+ }
68
+
69
  localStorage.setItem("iaConfig", JSON.stringify(config));
70
+ console.log("[iaConfigModule] Configuraci贸n guardada correctamente");
71
+ return true;
72
  } catch (e) {
73
  console.error("[iaConfigModule] Error guardando config:", e);
74
+ return false;
75
  }
76
  }
77
 
78
  function clone(obj) {
79
+ if (!obj) return null;
80
+ try {
81
+ return structuredClone(obj);
82
+ } catch {
83
+ return JSON.parse(JSON.stringify(obj));
84
+ }
85
  }
86
 
87
  function loadConfig() {
88
  let config;
89
  try {
90
+ const stored = localStorage.getItem("iaConfig");
91
+ if (stored) {
92
+ config = JSON.parse(stored);
93
+ // Validar estructura b谩sica
94
+ if (!config.llm || !config.transcription) {
95
+ throw new Error("Estructura de configuraci贸n inv谩lida");
96
+ }
97
+ } else {
98
+ config = clone(defaultConfig);
99
+ }
100
+ } catch (error) {
101
+ console.warn("[iaConfigModule] Error cargando config, usando default:", error);
102
  config = clone(defaultConfig);
103
  }
104
 
105
+ // Asegurar estructura completa
106
+ config = {
107
+ ...clone(defaultConfig),
108
+ ...config,
109
+ llm: {
110
+ ...defaultConfig.llm,
111
+ ...config.llm,
112
+ apiKeys: {
113
+ ...defaultConfig.llm.apiKeys,
114
+ ...(config.llm?.apiKeys || {})
115
+ }
116
+ },
117
+ transcription: {
118
+ ...defaultConfig.transcription,
119
+ ...config.transcription,
120
+ apiKeys: {
121
+ ...defaultConfig.transcription.apiKeys,
122
+ ...(config.transcription?.apiKeys || {})
123
+ },
124
+ models: {
125
+ ...defaultConfig.transcription.models,
126
+ ...(config.transcription?.models || {})
127
+ }
128
+ }
129
+ };
130
+
131
+ // Migraci贸n: transcription apiKey 煤nico -> apiKeys
132
  if (config.transcription.apiKey !== undefined) {
 
133
  const oldKey = config.transcription.apiKey;
134
+ const provider = config.transcription.provider || "openai";
135
+ config.transcription.apiKeys = {
136
+ ...config.transcription.apiKeys,
137
+ [provider]: oldKey
138
+ };
 
139
  delete config.transcription.apiKey;
140
+ delete config.transcription.model; // Tambi茅n eliminar model 煤nico si existe
141
  saveConfig(config);
142
+ console.log("[iaConfigModule] Migrado transcription.apiKey a apiKeys");
143
  }
144
 
145
+ // Migraci贸n: LLM apiKey 煤nico -> apiKeys
146
  if (config.llm.apiKey !== undefined) {
147
+ const oldKey = config.llm.apiKey;
148
+ const provider = config.llm.provider || "deepseek";
149
+ config.llm.apiKeys = {
150
+ ...config.llm.apiKeys,
151
+ [provider]: oldKey
152
+ };
153
  delete config.llm.apiKey;
154
  saveConfig(config);
155
+ console.log("[iaConfigModule] Migrado llm.apiKey a apiKeys");
156
  }
157
 
158
+ // Corregir modelos obsoletos o inexistentes de DeepSeek
159
+ if (config.llm.provider === "deepseek") {
160
+ const validDeepSeekModels = ["deepseek-chat", "deepseek-reasoner"];
161
+ if (!validDeepSeekModels.includes(config.llm.model)) {
162
+ config.llm.model = "deepseek-chat";
163
+ console.log("[iaConfigModule] Corregido modelo DeepSeek a deepseek-chat");
164
+ saveConfig(config);
165
+ }
166
  }
167
+
168
+ // Corregir modelos inexistentes de OpenAI (GPT-5 no existe a煤n)
169
+ if (config.llm.provider === "openai") {
170
+ const validOpenAIModels = [
171
+ "gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4",
172
+ "gpt-3.5-turbo", "o1-preview", "o1-mini"
173
+ ];
174
+
175
+ // Lista de modelos fict铆cios a corregir
176
+ const fakeModels = [
177
+ "gpt-5", "gpt-5-mini", "gpt-5-nano", "gpt-5-chat-latest",
178
+ "gpt-5-2025-05-01", "gpt-5-mini-2025-05-01", "chatgpt-4o-latest",
179
+ "o4-mini-2025-04-16"
180
+ ];
181
+
182
+ if (fakeModels.includes(config.llm.model) || !validOpenAIModels.includes(config.llm.model)) {
183
+ config.llm.model = "gpt-4o"; // Usar el mejor modelo disponible real
184
+ console.log("[iaConfigModule] Corregido modelo OpenAI fict铆cio a gpt-4o");
185
+ saveConfig(config);
186
+ }
187
+ }
188
+
189
+ // Validar modelos de transcripci贸n
190
+ if (config.transcription.provider === "deepgram") {
191
+ const validDeepgramModels = ["nova-2", "nova", "enhanced", "base"];
192
+ const currentModel = config.transcription.models?.deepgram;
193
+ if (!validDeepgramModels.includes(currentModel)) {
194
+ config.transcription.models.deepgram = "nova-2";
195
+ console.log("[iaConfigModule] Corregido modelo Deepgram a nova-2");
196
+ saveConfig(config);
197
+ }
198
  }
199
 
200
  return config;
201
  }
202
+
203
  export function getIaConfig() {
204
  return loadConfig();
205
  }
206
 
207
+ export function validateApiKey(provider, apiKey) {
208
+ if (!apiKey || apiKey.trim() === '') return false;
209
+
210
+ switch (provider) {
211
+ case 'openai':
212
+ return apiKey.startsWith('sk-') && apiKey.length >= 20;
213
+ case 'deepseek':
214
+ return apiKey.startsWith('sk-') && apiKey.length >= 20;
215
+ case 'deepgram':
216
+ return apiKey.length >= 30; // Deepgram keys son m谩s largas
217
+ default:
218
+ return apiKey.length >= 10;
219
+ }
220
+ }
221
+
222
  export function renderIaConfigForm(containerId) {
223
  let config = loadConfig();
224
  const container = document.getElementById(containerId);
225
+
226
  if (!container) {
227
  console.error(`[iaConfigModule] No se encontr贸 el contenedor '${containerId}'`);
228
+ const errorDiv = document.createElement('div');
229
+ errorDiv.style.cssText = 'color:red; padding:10px; border:1px solid red; margin:10px; border-radius:5px;';
230
+ errorDiv.innerHTML = `[Error] No se encontr贸 el contenedor '${containerId}' para la configuraci贸n IA.`;
231
+ document.body.appendChild(errorDiv);
232
  return;
233
  }
234
 
235
  function maskApiKey(key) {
236
+ if (!key || key.trim() === '') return '';
237
+ if (key.length <= 8) return '*'.repeat(key.length);
238
+ return key.substring(0, 4) + '-****-' + key.slice(-4);
239
+ }
240
+
241
+ function showError(message) {
242
+ const existingError = container.querySelector('.config-error');
243
+ if (existingError) existingError.remove();
244
+
245
+ const errorDiv = document.createElement('div');
246
+ errorDiv.className = 'config-error bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4';
247
+ errorDiv.innerHTML = `<i class="fas fa-exclamation-triangle mr-2"></i>${message}`;
248
+ container.insertBefore(errorDiv, container.firstChild);
249
+
250
+ setTimeout(() => errorDiv.remove(), 5000);
251
  }
252
 
253
  container.innerHTML = `
254
  <div class="flex justify-between items-center mb-6 border-b pb-2 border-blue-100">
255
+ <h2 class="text-xl font-bold text-blue-700 flex items-center">
256
+ <i class='fas fa-cogs mr-2'></i>Configurar Proveedores IA
257
+ </h2>
258
+ <button id="btnCloseConfig" type="button" class="text-gray-500 hover:text-blue-600 text-2xl focus:outline-none" aria-label="Cerrar">
259
+ <i class="fas fa-times"></i>
260
+ </button>
261
  </div>
262
+
263
  <form id="iaConfigForm" class="space-y-6">
264
+ <!-- Secci贸n LLM -->
265
  <div class="bg-blue-50 p-4 rounded-lg border border-blue-100">
266
+ <label class="block font-semibold text-blue-800 mb-2">
267
+ <i class="fas fa-brain mr-2"></i>Proveedor LLM
268
+ </label>
269
+ <select id="llmProvider" class="w-full mb-3 p-2 rounded border border-gray-300 focus:ring-2 focus:ring-blue-300">
270
+ ${llmProviders.map(p => `<option value="${p.value}">${p.name}</option>`).join('')}
271
+ </select>
272
  <div class="flex items-center mb-3">
273
+ <input type="password" id="llmApiKey" class="flex-1 p-2 rounded border border-gray-300 mr-2 focus:ring-2 focus:ring-blue-300"
274
+ placeholder="API Key LLM (requerida)" autocomplete="off">
275
+ <button class="text-blue-700 hover:text-blue-900 px-3 py-2 rounded focus:outline-none border border-blue-200 bg-white hover:bg-blue-50"
276
+ type="button" id="toggleLlmApiKey">
277
+ <i class="fas fa-eye"></i>
278
+ </button>
279
  </div>
280
  <select id="llmModel" class="w-full p-2 rounded border border-gray-300 focus:ring-2 focus:ring-blue-300"></select>
281
+ <div id="llmKeyStatus" class="text-xs mt-2 hidden"></div>
282
  </div>
283
+
284
+ <!-- Secci贸n Transcripci贸n -->
285
  <div class="bg-purple-50 p-4 rounded-lg border border-purple-100">
286
+ <label class="block font-semibold text-purple-800 mb-2">
287
+ <i class="fas fa-microphone mr-2"></i>Proveedor Transcripci贸n
288
+ </label>
289
+ <select id="transProvider" class="w-full mb-3 p-2 rounded border border-gray-300 focus:ring-2 focus:ring-purple-300">
290
+ ${transcriptionProviders.map(p => `<option value="${p.value}">${p.name}</option>`).join('')}
291
+ </select>
292
  <div class="flex items-center mb-3">
293
+ <input type="password" id="transApiKey" class="flex-1 p-2 rounded border border-gray-300 mr-2 focus:ring-2 focus:ring-purple-300"
294
+ placeholder="API Key Transcripci贸n (requerida)" autocomplete="off">
295
+ <button class="text-purple-700 hover:text-purple-900 px-3 py-2 rounded focus:outline-none border border-purple-200 bg-white hover:bg-purple-50"
296
+ type="button" id="toggleTransApiKey">
297
+ <i class="fas fa-eye"></i>
298
+ </button>
299
  </div>
300
  <select id="transModel" class="w-full p-2 rounded border border-gray-300 focus:ring-2 focus:ring-purple-300"></select>
301
+ <div id="transKeyStatus" class="text-xs mt-2 hidden"></div>
302
  </div>
303
+
304
+ <button type="submit" id="saveConfigBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 rounded-lg shadow transition-all duration-200 flex items-center justify-center text-lg">
305
+ <i class="fas fa-save mr-2"></i>Guardar configuraci贸n
306
+ </button>
307
+ </form>
308
+ `;
309
+
310
+ // Event listeners
311
+ const closeBtn = document.getElementById('btnCloseConfig');
312
+ closeBtn?.addEventListener('click', () => {
313
+ const modal = document.getElementById('configModal');
314
+ modal?.classList.remove('active');
315
+ });
316
+
317
+ // Valores iniciales
318
+ document.getElementById('llmProvider').value = config.llm.provider;
319
+ document.getElementById('llmApiKey').value = maskApiKey(config.llm.apiKeys[config.llm.provider] || '');
320
+ document.getElementById('transProvider').value = config.transcription.provider;
321
+ document.getElementById('transApiKey').value = maskApiKey(config.transcription.apiKeys[config.transcription.provider] || '');
322
+
323
+ // Toggle password visibility
324
+ document.getElementById('toggleLlmApiKey').addEventListener('click', () => {
325
+ const input = document.getElementById('llmApiKey');
326
+ const icon = document.querySelector('#toggleLlmApiKey i');
327
+ if (input.type === 'password') {
328
+ input.type = 'text';
329
+ icon.className = 'fas fa-eye-slash';
 
330
  } else {
331
+ input.type = 'password';
332
+ icon.className = 'fas fa-eye';
333
+ }
334
+ });
335
+
336
+ document.getElementById('toggleTransApiKey').addEventListener('click', () => {
337
+ const input = document.getElementById('transApiKey');
338
+ const icon = document.querySelector('#toggleTransApiKey i');
339
+ if (input.type === 'password') {
340
+ input.type = 'text';
341
+ icon.className = 'fas fa-eye-slash';
342
+ } else {
343
+ input.type = 'password';
344
+ icon.className = 'fas fa-eye';
345
+ }
346
+ });
347
+
348
+ function updateLlmModels() {
349
+ const provider = document.getElementById('llmProvider').value;
350
+ const providerObj = llmProviders.find(p => p.value === provider);
351
+ if (!providerObj) {
352
+ console.error('[iaConfigModule] Proveedor LLM no encontrado:', provider);
353
+ return;
354
+ }
355
+
356
+ const models = providerObj.models;
357
+ const modelSelect = document.getElementById('llmModel');
358
+ modelSelect.innerHTML = models.map(m => `<option value="${m}">${m}</option>`).join('');
359
+
360
+ // Seleccionar modelo actual o el primero si no es v谩lido
361
+ const currentModel = config.llm.model;
362
+ if (models.includes(currentModel)) {
363
+ modelSelect.value = currentModel;
364
+ } else {
365
+ modelSelect.value = models[0];
366
+ config.llm.model = models[0];
367
  }
 
368
  }
369
 
370
+ function updateTransModels() {
371
+ const provider = document.getElementById('transProvider').value;
372
+ const providerObj = transcriptionProviders.find(p => p.value === provider);
373
+ if (!providerObj) {
374
+ console.error('[iaConfigModule] Proveedor de transcripci贸n no encontrado:', provider);
375
+ return;
376
+ }
377
+
378
+ const models = providerObj.models;
379
+ const modelSelect = document.getElementById('transModel');
380
+ modelSelect.innerHTML = models.map(m => `<option value="${m}">${m}</option>`).join('');
381
+
382
+ // Seleccionar modelo actual o el primero
383
+ const currentModel = config.transcription.models[provider];
384
+ if (models.includes(currentModel)) {
385
+ modelSelect.value = currentModel;
386
+ } else {
387
+ modelSelect.value = models[0];
388
+ }
389
+
390
+ // Actualizar API key mostrada
391
+ const apiKey = config.transcription.apiKeys[provider] || '';
392
+ document.getElementById('transApiKey').value = maskApiKey(apiKey);
393
  }
394
 
395
+ // Event listeners para cambios de proveedor
396
+ document.getElementById('llmProvider').addEventListener('change', () => {
397
+ const provider = document.getElementById('llmProvider').value;
398
+ updateLlmModels();
399
+
400
+ // Mostrar la API key correspondiente
401
+ const apiKey = config.llm.apiKeys[provider] || '';
402
+ document.getElementById('llmApiKey').value = maskApiKey(apiKey);
403
+ });
404
 
405
+ document.getElementById('transProvider').addEventListener('change', updateTransModels);
 
 
 
406
 
407
+ // Inicializar modelos
408
+ updateLlmModels();
409
+ updateTransModels();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
 
411
+ // Validaci贸n en tiempo real
412
+ function validateKeyInput(inputId, provider, statusId) {
413
+ const input = document.getElementById(inputId);
414
+ const status = document.getElementById(statusId);
415
+
416
+ input.addEventListener('input', () => {
417
+ const key = input.value.trim();
418
+ if (key === '' || key.includes('*')) {
419
+ status.classList.add('hidden');
420
+ return;
421
+ }
422
+
423
+ const isValid = validateApiKey(provider, key);
424
+ status.classList.remove('hidden');
425
+
426
+ if (isValid) {
427
+ status.className = 'text-xs mt-2 text-green-600';
428
+ status.innerHTML = '<i class="fas fa-check mr-1"></i>Formato v谩lido';
429
+ } else {
430
+ status.className = 'text-xs mt-2 text-red-600';
431
+ status.innerHTML = '<i class="fas fa-exclamation-triangle mr-1"></i>Formato inv谩lido';
432
+ }
433
+ });
434
+ }
435
 
436
+ // No podemos validar din谩micamente porque el provider puede cambiar
437
+ // Validaremos en el submit
438
+
439
+ // Submit form
440
+ document.getElementById('iaConfigForm').addEventListener('submit', (e) => {
441
+ e.preventDefault();
 
 
 
 
 
 
 
 
 
 
 
 
442
 
443
+ try {
444
+ const prevConfig = config;
445
+ const newConfig = clone(prevConfig);
446
+
447
+ // Procesar LLM
448
+ const llmProvider = document.getElementById('llmProvider').value;
449
+ const llmModelValue = document.getElementById('llmModel').value;
450
+ const rawLlmKey = document.getElementById('llmApiKey').value.trim();
451
+
452
+ const oldLlmKey = prevConfig.llm.apiKeys[llmProvider] || '';
453
+ const newLlmKey = (rawLlmKey === maskApiKey(oldLlmKey)) ? oldLlmKey : rawLlmKey;
454
+
455
+ // Validar LLM API key
456
+ if (!newLlmKey || !validateApiKey(llmProvider, newLlmKey)) {
457
+ showError(`API Key de ${llmProvider.toUpperCase()} inv谩lida o faltante`);
458
+ return;
459
+ }
460
+
461
+ newConfig.llm = {
462
+ provider: llmProvider,
463
+ model: llmModelValue,
464
+ apiKeys: { ...prevConfig.llm.apiKeys, [llmProvider]: newLlmKey }
465
+ };
466
+
467
+ // Procesar Transcripci贸n
468
+ const transProvider = document.getElementById('transProvider').value;
469
+ const transModelValue = document.getElementById('transModel').value;
470
+ const rawTransKey = document.getElementById('transApiKey').value.trim();
471
+
472
+ const oldTransKey = prevConfig.transcription.apiKeys[transProvider] || '';
473
+ const newTransKey = (rawTransKey === maskApiKey(oldTransKey)) ? oldTransKey : rawTransKey;
474
+
475
+ // Validar transcription API key
476
+ if (!newTransKey || !validateApiKey(transProvider, newTransKey)) {
477
+ showError(`API Key de ${transProvider === 'openai' ? 'OpenAI' : 'Deepgram'} inv谩lida o faltante`);
478
+ return;
479
+ }
480
 
481
+ newConfig.transcription = {
482
+ provider: transProvider,
483
+ apiKeys: { ...prevConfig.transcription.apiKeys, [transProvider]: newTransKey },
484
+ models: { ...prevConfig.transcription.models, [transProvider]: transModelValue }
485
+ };
486
+
487
+ // Guardar configuraci贸n
488
+ if (!saveConfig(newConfig)) {
489
+ showError('Error guardando la configuraci贸n. Intente nuevamente.');
490
+ return;
491
+ }
492
+
493
+ config = newConfig;
494
+
495
+ // Disparar evento de cambio
496
+ document.dispatchEvent(new CustomEvent('iaConfigChanged', { detail: newConfig }));
497
+
498
+ // Actualizar campos con valores enmascarados
499
+ document.getElementById('llmApiKey').value = maskApiKey(newLlmKey);
500
+ document.getElementById('transApiKey').value = maskApiKey(newTransKey);
501
+ document.getElementById('llmApiKey').type = 'password';
502
+ document.getElementById('transApiKey').type = 'password';
503
+
504
+ // Resetear iconos de visibilidad
505
+ document.querySelector('#toggleLlmApiKey i').className = 'fas fa-eye';
506
+ document.querySelector('#toggleTransApiKey i').className = 'fas fa-eye';
507
+
508
+ // Mostrar mensaje de 茅xito
509
+ let successMsg = document.getElementById('iaConfigSavedMsg');
510
+ if (!successMsg) {
511
+ successMsg = document.createElement('div');
512
+ successMsg.id = 'iaConfigSavedMsg';
513
+ successMsg.className = 'fixed left-1/2 top-6 -translate-x-1/2 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg text-lg z-50 transform transition-all duration-300';
514
+ successMsg.innerHTML = '<i class="fas fa-check-circle mr-2"></i>隆Configuraci贸n guardada correctamente!';
515
+ document.body.appendChild(successMsg);
516
+ } else {
517
+ successMsg.style.display = 'block';
518
+ }
519
+
520
+ setTimeout(() => {
521
+ if (successMsg) successMsg.style.display = 'none';
522
+ }, 3000);
523
+
524
+ // Cerrar modal
525
+ const modal = document.getElementById('configModal');
526
+ if (modal) {
527
+ modal.classList.remove('active');
528
+ }
529
+
530
+ } catch (error) {
531
+ console.error('[iaConfigModule] Error en submit:', error);
532
+ showError('Error inesperado guardando la configuraci贸n');
533
+ }
534
  });
535
  }