iatoolkit 0.91.1__py3-none-any.whl → 1.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. iatoolkit/__init__.py +6 -4
  2. iatoolkit/base_company.py +0 -16
  3. iatoolkit/cli_commands.py +3 -14
  4. iatoolkit/common/exceptions.py +1 -0
  5. iatoolkit/common/interfaces/__init__.py +0 -0
  6. iatoolkit/common/interfaces/asset_storage.py +34 -0
  7. iatoolkit/common/interfaces/database_provider.py +43 -0
  8. iatoolkit/common/model_registry.py +159 -0
  9. iatoolkit/common/routes.py +47 -5
  10. iatoolkit/common/util.py +32 -13
  11. iatoolkit/company_registry.py +5 -0
  12. iatoolkit/core.py +51 -20
  13. iatoolkit/infra/connectors/file_connector_factory.py +1 -0
  14. iatoolkit/infra/connectors/s3_connector.py +4 -2
  15. iatoolkit/infra/llm_providers/__init__.py +0 -0
  16. iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
  17. iatoolkit/infra/{gemini_adapter.py → llm_providers/gemini_adapter.py} +11 -17
  18. iatoolkit/infra/{openai_adapter.py → llm_providers/openai_adapter.py} +41 -7
  19. iatoolkit/infra/llm_proxy.py +235 -134
  20. iatoolkit/infra/llm_response.py +5 -0
  21. iatoolkit/locales/en.yaml +158 -2
  22. iatoolkit/locales/es.yaml +158 -0
  23. iatoolkit/repositories/database_manager.py +52 -47
  24. iatoolkit/repositories/document_repo.py +7 -0
  25. iatoolkit/repositories/filesystem_asset_repository.py +36 -0
  26. iatoolkit/repositories/llm_query_repo.py +2 -0
  27. iatoolkit/repositories/models.py +72 -79
  28. iatoolkit/repositories/profile_repo.py +59 -3
  29. iatoolkit/repositories/vs_repo.py +22 -24
  30. iatoolkit/services/company_context_service.py +126 -53
  31. iatoolkit/services/configuration_service.py +299 -73
  32. iatoolkit/services/dispatcher_service.py +21 -3
  33. iatoolkit/services/file_processor_service.py +0 -5
  34. iatoolkit/services/history_manager_service.py +43 -24
  35. iatoolkit/services/knowledge_base_service.py +425 -0
  36. iatoolkit/{infra/llm_client.py → services/llm_client_service.py} +38 -29
  37. iatoolkit/services/load_documents_service.py +26 -48
  38. iatoolkit/services/profile_service.py +32 -4
  39. iatoolkit/services/prompt_service.py +32 -30
  40. iatoolkit/services/query_service.py +51 -26
  41. iatoolkit/services/sql_service.py +122 -74
  42. iatoolkit/services/tool_service.py +26 -11
  43. iatoolkit/services/user_session_context_service.py +115 -63
  44. iatoolkit/static/js/chat_main.js +44 -4
  45. iatoolkit/static/js/chat_model_selector.js +227 -0
  46. iatoolkit/static/js/chat_onboarding_button.js +1 -1
  47. iatoolkit/static/js/chat_reload_button.js +4 -1
  48. iatoolkit/static/styles/chat_iatoolkit.css +58 -2
  49. iatoolkit/static/styles/llm_output.css +34 -1
  50. iatoolkit/system_prompts/query_main.prompt +26 -2
  51. iatoolkit/templates/base.html +13 -0
  52. iatoolkit/templates/chat.html +45 -2
  53. iatoolkit/templates/onboarding_shell.html +0 -1
  54. iatoolkit/views/base_login_view.py +7 -2
  55. iatoolkit/views/chat_view.py +76 -0
  56. iatoolkit/views/configuration_api_view.py +163 -0
  57. iatoolkit/views/load_document_api_view.py +14 -10
  58. iatoolkit/views/login_view.py +8 -3
  59. iatoolkit/views/rag_api_view.py +216 -0
  60. iatoolkit/views/users_api_view.py +33 -0
  61. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/METADATA +4 -4
  62. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/RECORD +66 -58
  63. iatoolkit/repositories/tasks_repo.py +0 -52
  64. iatoolkit/services/search_service.py +0 -55
  65. iatoolkit/services/tasks_service.py +0 -188
  66. iatoolkit/views/tasks_api_view.py +0 -72
  67. iatoolkit/views/tasks_review_api_view.py +0 -55
  68. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/WHEEL +0 -0
  69. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/licenses/LICENSE +0 -0
  70. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/licenses/LICENSE_COMMUNITY.md +0 -0
  71. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/top_level.txt +0 -0
@@ -15,73 +15,101 @@ class UserSessionContextService:
15
15
  Esto mejora la atomicidad y la eficiencia.
16
16
  """
17
17
 
18
- def _get_session_key(self, company_short_name: str, user_identifier: str) -> Optional[str]:
18
+ def _get_session_key(self, company_short_name: str, user_identifier: str, model: str = None) -> Optional[str]:
19
19
  """Devuelve la clave única de Redis para el Hash de sesión del usuario."""
20
20
  user_identifier = (user_identifier or "").strip()
21
21
  if not company_short_name or not user_identifier:
22
22
  return None
23
- return f"session:{company_short_name}/{user_identifier}"
24
23
 
25
- def clear_all_context(self, company_short_name: str, user_identifier: str):
26
- """Limpia el contexto del LLM en la sesión para un usuario de forma atómica."""
27
- session_key = self._get_session_key(company_short_name, user_identifier)
24
+ model_key = "" if not model else f"-{model}"
25
+ return f"session:{company_short_name}/{user_identifier}{model_key}"
26
+
27
+ def clear_all_context(self, company_short_name: str, user_identifier: str, model: str = None):
28
+ """Clears LLM-related context for a user (history and response IDs), preserving profile_data."""
29
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
28
30
  if session_key:
29
- # RedisSessionManager.remove(session_key)
30
31
  # 'profile_data' should not be deleted
31
- RedisSessionManager.hdel(session_key, 'context_version')
32
- RedisSessionManager.hdel(session_key, 'context_history')
33
- RedisSessionManager.hdel(session_key, 'last_response_id')
32
+ RedisSessionManager.hdel(session_key, "context_version")
33
+ RedisSessionManager.hdel(session_key, "context_history")
34
+ RedisSessionManager.hdel(session_key, "last_response_id")
34
35
 
35
- def clear_llm_history(self, company_short_name: str, user_identifier: str):
36
- """Limpia solo los campos relacionados con el historial del LLM (ID y chat)."""
37
- session_key = self._get_session_key(company_short_name, user_identifier)
36
+ def clear_llm_history(self, company_short_name: str, user_identifier: str, model: str = None):
37
+ """Clears only LLM history fields (last_response_id and context_history)."""
38
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
38
39
  if session_key:
39
- RedisSessionManager.hdel(session_key, 'last_response_id', 'context_history')
40
+ RedisSessionManager.hdel(session_key, "last_response_id", "context_history")
40
41
 
41
- def get_last_response_id(self, company_short_name: str, user_identifier: str) -> Optional[str]:
42
- session_key = self._get_session_key(company_short_name, user_identifier)
42
+ def get_last_response_id(self, company_short_name: str, user_identifier: str, model: str = None) -> Optional[str]:
43
+ """Returns the last LLM response ID for this user/model combination."""
44
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
43
45
  if not session_key:
44
46
  return None
45
- return RedisSessionManager.hget(session_key, 'last_response_id')
46
-
47
- def save_last_response_id(self, company_short_name: str, user_identifier: str, response_id: str):
48
- session_key = self._get_session_key(company_short_name, user_identifier)
47
+ return RedisSessionManager.hget(session_key, "last_response_id")
48
+
49
+ def save_last_response_id(self,
50
+ company_short_name: str,
51
+ user_identifier: str,
52
+ response_id: str,
53
+ model: str = None,
54
+ ):
55
+ """Persists the last LLM response ID for this user/model combination."""
56
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
49
57
  if session_key:
50
- RedisSessionManager.hset(session_key, 'last_response_id', response_id)
58
+ RedisSessionManager.hset(session_key, "last_response_id", response_id)
51
59
 
52
- def get_initial_response_id(self, company_short_name: str, user_identifier: str) -> Optional[str]:
60
+ def get_initial_response_id(self,
61
+ company_short_name: str,
62
+ user_identifier: str,
63
+ model: str = None,
64
+ ) -> Optional[str]:
53
65
  """
54
- Obtiene el ID de respuesta inicial desde la sesión del usuario.
55
- Este ID corresponde al estado del LLM justo después de haber configurado el contexto.
66
+ Returns the initial LLM response ID for this user/model combination.
67
+ This ID represents the state right after the context was set on the LLM.
56
68
  """
57
- session_key = self._get_session_key(company_short_name, user_identifier)
69
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
58
70
  if not session_key:
59
71
  return None
60
- return RedisSessionManager.hget(session_key, 'initial_response_id')
61
-
62
- def save_initial_response_id(self, company_short_name: str, user_identifier: str, response_id: str):
63
- """
64
- Guarda el ID de respuesta inicial en la sesión del usuario.
65
- """
66
- session_key = self._get_session_key(company_short_name, user_identifier)
72
+ return RedisSessionManager.hget(session_key, "initial_response_id")
73
+
74
+ def save_initial_response_id(self,
75
+ company_short_name: str,
76
+ user_identifier: str,
77
+ response_id: str,
78
+ model: str = None,
79
+ ):
80
+ """Persists the initial LLM response ID for this user/model combination."""
81
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
67
82
  if session_key:
68
- RedisSessionManager.hset(session_key, 'initial_response_id', response_id)
69
-
70
- def save_context_history(self, company_short_name: str, user_identifier: str, context_history: List[Dict]):
71
- session_key = self._get_session_key(company_short_name, user_identifier)
83
+ RedisSessionManager.hset(session_key, "initial_response_id", response_id)
84
+
85
+ def save_context_history(
86
+ self,
87
+ company_short_name: str,
88
+ user_identifier: str,
89
+ context_history: List[Dict],
90
+ model: str = None,
91
+ ):
92
+ """Serializes and stores the context history for this user/model combination."""
93
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
72
94
  if session_key:
73
95
  try:
74
96
  history_json = json.dumps(context_history)
75
- RedisSessionManager.hset(session_key, 'context_history', history_json)
97
+ RedisSessionManager.hset(session_key, "context_history", history_json)
76
98
  except (TypeError, ValueError) as e:
77
- logging.error(f"Error al serializar context_history para {session_key}: {e}")
78
-
79
- def get_context_history(self, company_short_name: str, user_identifier: str) -> Optional[List[Dict]]:
80
- session_key = self._get_session_key(company_short_name, user_identifier)
99
+ logging.error(f"Error serializing context_history for {session_key}: {e}")
100
+
101
+ def get_context_history(
102
+ self,
103
+ company_short_name: str,
104
+ user_identifier: str,
105
+ model: str = None,
106
+ ) -> Optional[List[Dict]]:
107
+ """Reads and deserializes the context history for this user/model combination."""
108
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
81
109
  if not session_key:
82
- return None
110
+ return []
83
111
 
84
- history_json = RedisSessionManager.hget(session_key, 'context_history')
112
+ history_json = RedisSessionManager.hget(session_key, "context_history")
85
113
  if not history_json:
86
114
  return []
87
115
 
@@ -113,37 +141,61 @@ class UserSessionContextService:
113
141
  except json.JSONDecodeError:
114
142
  return {}
115
143
 
116
- def save_context_version(self, company_short_name: str, user_identifier: str, version: str):
117
- session_key = self._get_session_key(company_short_name, user_identifier)
144
+ def save_context_version(self,
145
+ company_short_name: str,
146
+ user_identifier: str,
147
+ version: str,
148
+ model: str = None,
149
+ ):
150
+ """Saves the context version for this user/model combination."""
151
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
118
152
  if session_key:
119
- RedisSessionManager.hset(session_key, 'context_version', version)
120
-
121
- def get_context_version(self, company_short_name: str, user_identifier: str) -> Optional[str]:
122
- session_key = self._get_session_key(company_short_name, user_identifier)
153
+ RedisSessionManager.hset(session_key, "context_version", version)
154
+
155
+ def get_context_version(self,
156
+ company_short_name: str,
157
+ user_identifier: str,
158
+ model: str = None,
159
+ ) -> Optional[str]:
160
+ """Returns the context version for this user/model combination."""
161
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
123
162
  if not session_key:
124
163
  return None
125
- return RedisSessionManager.hget(session_key, 'context_version')
126
-
127
- def save_prepared_context(self, company_short_name: str, user_identifier: str, context: str, version: str):
128
- """Guarda un contexto de sistema pre-renderizado y su versión, listos para ser enviados al LLM."""
129
- session_key = self._get_session_key(company_short_name, user_identifier)
164
+ return RedisSessionManager.hget(session_key, "context_version")
165
+
166
+ def save_prepared_context(self,
167
+ company_short_name: str,
168
+ user_identifier: str,
169
+ context: str,
170
+ version: str,
171
+ model: str = None,
172
+ ):
173
+ """Stores a pre-rendered system context and its version, ready to be sent to the LLM."""
174
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
130
175
  if session_key:
131
- RedisSessionManager.hset(session_key, 'prepared_context', context)
132
- RedisSessionManager.hset(session_key, 'prepared_context_version', version)
133
-
134
- def get_and_clear_prepared_context(self, company_short_name: str, user_identifier: str) -> tuple:
135
- """Obtiene el contexto preparado y su versión, y los elimina para asegurar que se usan una sola vez."""
136
- session_key = self._get_session_key(company_short_name, user_identifier)
176
+ RedisSessionManager.hset(session_key, "prepared_context", context)
177
+ RedisSessionManager.hset(session_key, "prepared_context_version", version)
178
+
179
+ def get_and_clear_prepared_context(self,
180
+ company_short_name: str,
181
+ user_identifier: str,
182
+ model: str = None,
183
+ ) -> tuple:
184
+ """
185
+ Atomically retrieves the prepared context and its version and then deletes them
186
+ to guarantee they are consumed only once.
187
+ """
188
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
137
189
  if not session_key:
138
190
  return None, None
139
191
 
140
192
  pipe = RedisSessionManager.pipeline()
141
- pipe.hget(session_key, 'prepared_context')
142
- pipe.hget(session_key, 'prepared_context_version')
143
- pipe.hdel(session_key, 'prepared_context', 'prepared_context_version')
193
+ pipe.hget(session_key, "prepared_context")
194
+ pipe.hget(session_key, "prepared_context_version")
195
+ pipe.hdel(session_key, "prepared_context", "prepared_context_version")
144
196
  results = pipe.execute()
145
197
 
146
- # results[0] es el contexto, results[1] es la versión
198
+ # results[0] is the context, results[1] is the version
147
199
  return (results[0], results[1]) if results else (None, None)
148
200
 
149
201
  # --- Métodos de Bloqueo ---
@@ -110,13 +110,48 @@ const handleChatMessage = async function () {
110
110
  prompt_name: promptName,
111
111
  client_data: clientData,
112
112
  files: filesBase64.map(f => ({ filename: f.name, content: f.base64 })),
113
- user_identifier: window.user_identifier
113
+ user_identifier: window.user_identifier,
114
+ model: (window.currentLlmModel || window.defaultLlmModel || '')
115
+
114
116
  };
115
117
 
116
118
  const responseData = await callToolkit("/api/llm_query", data, "POST");
117
119
  if (responseData && responseData.answer) {
118
- const answerSection = $('<div>').addClass('answer-section llm-output').append(responseData.answer);
119
- displayBotMessage(answerSection);
120
+ // CAMBIO: contenedor principal para la respuesta del bot
121
+ const botMessageContainer = $('<div>').addClass('bot-message-container');
122
+
123
+ // 1. Si hay reasoning_content, agregar el acordeón colapsable
124
+ if (responseData.reasoning_content) {
125
+ const uniqueId = 'reasoning-' + Date.now(); // ID único para el collapse
126
+
127
+ const reasoningBlock = $(`
128
+ <div class="reasoning-block">
129
+ <button class="reasoning-toggle btn btn-sm btn-link text-decoration-none p-0"
130
+ type="button"
131
+ data-bs-toggle="collapse"
132
+ data-bs-target="#${uniqueId}"
133
+ aria-expanded="false"
134
+ aria-controls="${uniqueId}">
135
+ <i class="bi bi-lightbulb me-1"></i> ${t_js('show_reasoning')}
136
+ </button>
137
+
138
+ <div class="collapse mt-2" id="${uniqueId}">
139
+ <div class="reasoning-card">
140
+ ${responseData.reasoning_content}
141
+ </div>
142
+ </div>
143
+ </div>
144
+ `);
145
+ botMessageContainer.append(reasoningBlock);
146
+ }
147
+
148
+ // 2. Agregar la respuesta final
149
+ const answerSection = $('<div>').addClass('answer-section llm-output').append(responseData.answer);
150
+ botMessageContainer.append(answerSection);
151
+
152
+ // 3. Mostrar el contenedor completo
153
+ displayBotMessage(botMessageContainer);
154
+
120
155
  }
121
156
  } catch (error) {
122
157
  if (error.name === 'AbortError') {
@@ -207,7 +242,12 @@ const toggleSendStopButtons = function (showStop) {
207
242
  * @returns {Promise<object|null>} The response data or null on error.
208
243
  */
209
244
  const callToolkit = async function(apiPath, data, method, timeoutMs = 500000) {
210
- const url = `${window.iatoolkit_base_url}/${window.companyShortName}${apiPath}`;
245
+ // normalize the url for avoiding double //
246
+ const base = (window.iatoolkit_base_url || '').replace(/\/+$/, '');
247
+ const company = (window.companyShortName || '').replace(/^\/+|\/+$/g, '');
248
+ const path = apiPath.startsWith('/') ? apiPath : `/${apiPath}`;
249
+ const url = `${base}/${company}${path}`;
250
+
211
251
 
212
252
  abortController = new AbortController();
213
253
  const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
@@ -0,0 +1,227 @@
1
+ // src/iatoolkit/static/js/chat_model_selector.js
2
+ // Gestión del selector de modelo LLM en la barra superior.
3
+
4
+ // Estado global del modelo actual (visible también para otros scripts)
5
+ window.currentLlmModel = window.currentLlmModel || null;
6
+
7
+ (function () {
8
+ /**
9
+ * Lee el modelo guardado en localStorage (si existe y es válido).
10
+ */
11
+ function loadStoredModelId() {
12
+ try {
13
+ const stored = localStorage.getItem('iatoolkit.selected_llm_model');
14
+ return stored || null;
15
+ } catch (e) {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Guarda el modelo seleccionado en localStorage para esta instancia de navegador.
22
+ * No es crítico: si falla, simplemente no persistimos.
23
+ */
24
+ function storeModelId(modelId) {
25
+ try {
26
+ if (!modelId) {
27
+ localStorage.removeItem('iatoolkit.selected_llm_model');
28
+ } else {
29
+ localStorage.setItem('iatoolkit.selected_llm_model', modelId);
30
+ }
31
+ } catch (e) {
32
+ // No hacemos nada: fallo silencioso
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Devuelve la lista de modelos disponibles desde la variable global.
38
+ */
39
+ function getAvailableModels() {
40
+ const raw = window.availableLlmModels;
41
+ if (!Array.isArray(raw)) {
42
+ return [];
43
+ }
44
+ return raw.map(m => ({
45
+ id: m.id,
46
+ label: m.label || m.id,
47
+ description: m.description || ''
48
+ })).filter(m => !!m.id);
49
+ }
50
+
51
+ /**
52
+ * Inicializa el estado de currentLlmModel usando SIEMPRE la config de company.yaml:
53
+ * 1) defaultLlmModel (company.yaml)
54
+ * 2) si no existe o no está en la lista, usa el primer modelo disponible.
55
+ *
56
+ * No se lee nada de localStorage en este punto: cada apertura de chat
57
+ * arranca desde la configuración de la compañía.
58
+ */
59
+ function initCurrentModel() {
60
+ const models = getAvailableModels();
61
+ const defaultId = (window.defaultLlmModel || '').trim() || null;
62
+
63
+ let resolved = null;
64
+
65
+ if (defaultId && models.some(m => m.id === defaultId)) {
66
+ resolved = defaultId;
67
+ } else if (models.length > 0) {
68
+ resolved = models[0].id;
69
+ }
70
+
71
+ window.currentLlmModel = resolved;
72
+ return resolved;
73
+ }
74
+
75
+ /**
76
+ * Pinta la lista de modelos en el popup y marca el seleccionado.
77
+ */
78
+ function renderModelList() {
79
+ const listEl = document.getElementById('llm-model-list');
80
+ if (!listEl) return;
81
+
82
+ const models = getAvailableModels();
83
+ const activeId = window.currentLlmModel;
84
+ listEl.innerHTML = '';
85
+
86
+ if (!models.length) {
87
+ const emptyItem = document.createElement('div');
88
+ emptyItem.className = 'list-group-item small text-muted';
89
+ emptyItem.textContent = 'No hay modelos configurados.';
90
+ listEl.appendChild(emptyItem);
91
+ return;
92
+ }
93
+
94
+ models.forEach(model => {
95
+ const item = document.createElement('button');
96
+ item.type = 'button';
97
+ item.className = 'list-group-item list-group-item-action small';
98
+
99
+ const isActive = model.id === activeId;
100
+ if (isActive) {
101
+ item.classList.add('active');
102
+ }
103
+
104
+ item.innerHTML = `
105
+ <div class="d-flex justify-content-between align-items-center">
106
+ <div>
107
+ <div class="fw-semibold">${model.label}</div>
108
+ ${model.description
109
+ ? `<div class="text-muted" style="font-size: 0.8rem;">${model.description}</div>`
110
+ : ''
111
+ }
112
+ </div>
113
+ ${isActive ? '<i class="bi bi-check-lg ms-2"></i>' : ''}
114
+ </div>
115
+ `;
116
+
117
+ item.addEventListener('click', () => {
118
+ selectModel(model.id);
119
+ });
120
+
121
+ listEl.appendChild(item);
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Actualiza el label del botón principal con el modelo actual.
127
+ */
128
+ function updateButtonLabel() {
129
+ const labelEl = document.getElementById('llm-model-button-label');
130
+ if (!labelEl) return;
131
+
132
+ const models = getAvailableModels();
133
+ const activeId = window.currentLlmModel;
134
+ const activeModel = models.find(m => m.id === activeId);
135
+
136
+ if (activeModel) {
137
+ labelEl.textContent = activeModel.label;
138
+ } else if (window.defaultLlmModel) {
139
+ labelEl.textContent = window.defaultLlmModel;
140
+ } else {
141
+ labelEl.textContent = 'Modelo IA';
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Selecciona un modelo: actualiza estado global, UI y almacenamiento local.
147
+ */
148
+ function selectModel(modelId) {
149
+ if (!modelId) return;
150
+
151
+ const models = getAvailableModels();
152
+ const exists = models.some(m => m.id === modelId);
153
+ if (!exists) return;
154
+
155
+ window.currentLlmModel = modelId;
156
+ storeModelId(modelId);
157
+ updateButtonLabel();
158
+ renderModelList();
159
+ hidePopup();
160
+
161
+ if (typeof toastr !== 'undefined') {
162
+ toastr.info(`Modelo actualizado a "${models.find(m => m.id === modelId).label}".`);
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Muestra/oculta el popup anclado al botón.
168
+ */
169
+ function togglePopup() {
170
+ const popup = document.getElementById('llm-model-popup');
171
+ const btn = document.getElementById('llm-model-button');
172
+ if (!popup || !btn) return;
173
+
174
+ const isVisible = popup.style.display === 'block';
175
+
176
+ if (isVisible) {
177
+ hidePopup();
178
+ } else {
179
+ const rect = btn.getBoundingClientRect();
180
+ popup.style.display = 'block';
181
+
182
+ // Posicionamos justo debajo del botón, alineado a la izquierda
183
+ popup.style.top = `${rect.bottom + window.scrollY + 4}px`;
184
+ popup.style.left = `${rect.left + window.scrollX}px`;
185
+ }
186
+ }
187
+
188
+ function hidePopup() {
189
+ const popup = document.getElementById('llm-model-popup');
190
+ if (popup) {
191
+ popup.style.display = 'none';
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Cierra el popup si el usuario hace click fuera.
197
+ */
198
+ function setupOutsideClickHandler() {
199
+ document.addEventListener('click', (event) => {
200
+ const popup = document.getElementById('llm-model-popup');
201
+ const btn = document.getElementById('llm-model-button');
202
+ if (!popup || !btn) return;
203
+
204
+ if (popup.style.display !== 'block') return;
205
+
206
+ if (!popup.contains(event.target) && !btn.contains(event.target)) {
207
+ hidePopup();
208
+ }
209
+ });
210
+ }
211
+
212
+ document.addEventListener('DOMContentLoaded', () => {
213
+ // Inicializar estado inicial del modelo
214
+ initCurrentModel();
215
+ updateButtonLabel();
216
+ renderModelList();
217
+ setupOutsideClickHandler();
218
+
219
+ const btn = document.getElementById('llm-model-button');
220
+ if (btn) {
221
+ btn.addEventListener('click', (event) => {
222
+ event.stopPropagation();
223
+ togglePopup();
224
+ });
225
+ }
226
+ });
227
+ })();
@@ -45,7 +45,7 @@
45
45
  if (elTitle) elTitle.textContent = c.title || '';
46
46
  if (elText) elText.innerHTML = c.text || '';
47
47
  if (elExample && c.example) {
48
- elExample.innerHTML = (t_js('example') + ': ' + c.example) || '';
48
+ elExample.innerHTML = ('Example ' + ': ' + c.example) || '';
49
49
  }
50
50
  else
51
51
  elExample.innerHTML = '';
@@ -18,7 +18,10 @@ $(document).ready(function () {
18
18
 
19
19
  // 2. prepare the api parameters
20
20
  const apiPath = '/api/init-context';
21
- const payload = {'user_identifier': window.user_identifier};
21
+ const payload = {
22
+ 'user_identifier': window.user_identifier,
23
+ 'model': (window.currentLlmModel || window.defaultLlmModel || '')
24
+ };
22
25
 
23
26
  // 3. make the call to callToolkit
24
27
  const data = await callToolkit(apiPath, payload, 'POST');
@@ -499,5 +499,61 @@ li {
499
499
  cursor: pointer;
500
500
  }
501
501
 
502
-
503
-
502
+ /* Popup de selección de modelo LLM - versión minimalista, sin branding de fondo */
503
+ .llm-model-popup {
504
+ background-color: #f9fafb; /* Fondo claro neutro */
505
+ border-radius: 0.5rem;
506
+ border: 1px solid #e5e7eb; /* Borde gris muy suave */
507
+ color: #111827; /* Texto principal en gris casi negro */
508
+ box-shadow: 0 10px 30px rgba(15, 23, 42, 0.12); /* Sombra suave y limpia */
509
+ }
510
+
511
+ .llm-model-popup .card-body {
512
+ padding-left: 0.9rem;
513
+ padding-right: 0.9rem;
514
+ }
515
+
516
+ .llm-model-popup .llm-model-subtitle {
517
+ font-size: 0.8rem;
518
+ color: #6b7280; /* Gris medio para el subtítulo */
519
+ }
520
+
521
+ .llm-model-popup .list-group-item {
522
+ border: none;
523
+ padding-top: 0.4rem;
524
+ padding-bottom: 0.4rem;
525
+ background-color: transparent;
526
+ color: inherit;
527
+ border-radius: 0.35rem;
528
+ transition:
529
+ background-color 0.15s ease-in-out,
530
+ transform 0.1s ease-in-out,
531
+ box-shadow 0.15s ease-in-out;
532
+ }
533
+
534
+ /* Descripción del modelo: mismo color que el nombre, pero un poco más suave */
535
+ .llm-model-popup .list-group-item .text-muted {
536
+ color: currentColor !important;
537
+ opacity: 0.75;
538
+ font-size: 0.8rem;
539
+ }
540
+
541
+ /* Hover más visible, pero manteniendo el estilo minimalista */
542
+ .llm-model-popup .list-group-item:hover {
543
+ background-color: #e5e7eb; /* Gris claro para resaltar bien */
544
+ transform: translateX(2px);
545
+ box-shadow: 0 2px 6px rgba(15, 23, 42, 0.12);
546
+ }
547
+
548
+ /* Item activo: resalta con el primario, pero fondo muy claro */
549
+ .llm-model-popup .list-group-item.active {
550
+ background-color: #eef2ff; /* Azul/gris muy claro tipo focus */
551
+ color: var(--brand-primary-color, #4C6A8D);
552
+ border: 1px solid var(--brand-primary-color, #4C6A8D);
553
+ box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.2);
554
+ }
555
+
556
+ .llm-model-popup .list-group-item.active .text-muted {
557
+ color: currentColor !important;
558
+ opacity: 0.85;
559
+ }
@@ -1,7 +1,7 @@
1
1
  /* BLOQUE GENERAL */
2
2
  .llm-output {
3
3
  margin: 0;
4
- padding: 20px;
4
+ padding: 14px;
5
5
  font-family: Arial, sans-serif;
6
6
  font-size: 16px;
7
7
  line-height: 1.5;
@@ -113,3 +113,36 @@
113
113
  .nowrap {
114
114
  white-space: nowrap;
115
115
  }
116
+
117
+ /* =========================================================
118
+ REASONING BLOCK (LLM)
119
+ ========================================================= */
120
+
121
+ .reasoning-block {
122
+ }
123
+
124
+ /* Botón de mostrar razonamiento */
125
+ .reasoning-toggle {
126
+ font-size: 13px;
127
+ color: #6c757d; /* text-secondary */
128
+ }
129
+
130
+ .reasoning-toggle:hover {
131
+ color: #495057;
132
+ text-decoration: underline;
133
+ }
134
+
135
+ /* Contenedor del reasoning */
136
+ .reasoning-card {
137
+ background-color: #f8f9fa; /* bg-light */
138
+ border-left: 3px solid #6c757d; /* border-secondary */
139
+ padding: 10px 14px;
140
+ font-size: 14px; /* consistente con llm-output */
141
+ line-height: 1.5;
142
+ color: #555;
143
+ white-space: pre-wrap;
144
+ max-height: 300px;
145
+ overflow-y: auto;
146
+ font-family: inherit; /* 👈 clave: usa la misma fuente base */
147
+ }
148
+